2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "ExternalPlayer.h"
11 #include "CompileInfo.h"
13 #include "ServiceBroker.h"
15 #include "application/Application.h"
16 #include "application/ApplicationComponents.h"
17 #include "application/ApplicationPowerHandling.h"
18 #include "cores/AudioEngine/Interfaces/AE.h"
19 #include "cores/DataCacheCore.h"
20 #include "dialogs/GUIDialogOK.h"
21 #include "filesystem/MusicDatabaseFile.h"
22 #include "guilib/GUIComponent.h"
23 #include "guilib/GUIWindowManager.h"
24 #include "threads/SystemClock.h"
25 #include "utils/RegExp.h"
26 #include "utils/StringUtils.h"
27 #include "utils/URIUtils.h"
28 #include "utils/Variant.h"
29 #include "utils/XMLUtils.h"
30 #include "utils/log.h"
31 #include "video/Bookmark.h"
32 #include "windowing/WinSystem.h"
33 #if defined(TARGET_WINDOWS)
34 #include "utils/CharsetConverter.h"
37 #if defined(TARGET_ANDROID)
38 #include "platform/android/activity/XBMCApp.h"
41 // If the process ends in less than this time (ms), we assume it's a launcher
42 // and wait for manual intervention before continuing
43 #define LAUNCHER_PROCESS_TIME 2000
44 // Time (ms) we give a process we sent a WM_QUIT to close before terminating
45 #define PROCESS_GRACE_TIME 3000
46 // Default time after which the item's playcount is incremented
47 #define DEFAULT_PLAYCOUNT_MIN_TIME 10
49 using namespace XFILE
;
50 using namespace std::chrono_literals
;
52 #if defined(TARGET_WINDOWS_DESKTOP)
56 CExternalPlayer::CExternalPlayer(IPlayerCallback
& callback
)
57 : IPlayer(callback
), CThread("ExternalPlayer"), m_playbackStartTime
{}
59 m_bAbortRequest
= false;
64 m_hideconsole
= false;
65 m_warpcursor
= WARP_NONE
;
68 m_playCountMinTime
= DEFAULT_PLAYCOUNT_MIN_TIME
;
69 m_playOneStackItem
= false;
72 #if defined(TARGET_WINDOWS_DESKTOP)
76 memset(&m_processInfo
, 0, sizeof(m_processInfo
));
80 CExternalPlayer::~CExternalPlayer()
85 bool CExternalPlayer::OpenFile(const CFileItem
& file
, const CPlayerOptions
&options
)
92 m_playbackStartTime
= std::chrono::steady_clock::now();
93 m_launchFilename
= file
.GetDynPath();
94 CLog::Log(LOGINFO
, "{}: {}", __FUNCTION__
, m_launchFilename
);
101 m_bIsPlaying
= false;
102 CLog::Log(LOGERROR
, "{} - Exception thrown", __FUNCTION__
);
107 bool CExternalPlayer::CloseFile(bool reopen
)
109 m_bAbortRequest
= true;
111 if (m_dialog
&& m_dialog
->IsActive()) m_dialog
->Close();
113 #if defined(TARGET_WINDOWS_DESKTOP)
114 if (m_bIsPlaying
&& m_processInfo
.hProcess
)
116 TerminateProcess(m_processInfo
.hProcess
, 1);
119 CServiceBroker::GetDataCacheCore().Reset();
123 bool CExternalPlayer::IsPlaying() const
128 void CExternalPlayer::Process()
130 std::string mainFile
= m_launchFilename
;
131 std::string archiveContent
;
133 if (m_args
.find("{0}") == std::string::npos
)
135 // Unwind archive names
136 CURL
url(m_launchFilename
);
137 if (url
.IsProtocol("zip") || url
.IsProtocol("rar") /* || url.IsProtocol("iso9660") ??*/ || url
.IsProtocol("udf"))
139 mainFile
= url
.GetHostName();
140 archiveContent
= url
.GetFileName();
142 if (url
.IsProtocol("musicdb"))
143 mainFile
= CMusicDatabaseFile::TranslateUrl(url
);
144 if (url
.IsProtocol("bluray"))
146 CURL
base(url
.GetHostName());
147 if (base
.IsProtocol("udf"))
149 mainFile
= base
.GetHostName(); /* image file */
150 archiveContent
= base
.GetFileName();
153 mainFile
= URIUtils::AddFileToFolder(base
.Get(), url
.GetFileName());
157 if (!m_filenameReplacers
.empty())
159 for (unsigned int i
= 0; i
< m_filenameReplacers
.size(); i
++)
161 std::vector
<std::string
> vecSplit
= StringUtils::Split(m_filenameReplacers
[i
], " , ");
163 // something is wrong, go to next substitution
164 if (vecSplit
.size() != 4)
167 std::string strMatch
= vecSplit
[0];
168 StringUtils::Replace(strMatch
, ",,",",");
169 bool bCaseless
= vecSplit
[3].find('i') != std::string::npos
;
170 CRegExp
regExp(bCaseless
, CRegExp::autoUtf8
);
172 if (!regExp
.RegComp(strMatch
.c_str()))
173 { // invalid regexp - complain in logs
174 CLog::Log(LOGERROR
, "{}: Invalid RegExp:'{}'", __FUNCTION__
, strMatch
);
178 if (regExp
.RegFind(mainFile
) > -1)
180 std::string strPat
= vecSplit
[1];
181 StringUtils::Replace(strPat
, ",,",",");
183 if (!regExp
.RegComp(strPat
.c_str()))
184 { // invalid regexp - complain in logs
185 CLog::Log(LOGERROR
, "{}: Invalid RegExp:'{}'", __FUNCTION__
, strPat
);
189 std::string strRep
= vecSplit
[2];
190 StringUtils::Replace(strRep
, ",,",",");
191 bool bGlobal
= vecSplit
[3].find('g') != std::string::npos
;
192 bool bStop
= vecSplit
[3].find('s') != std::string::npos
;
194 while ((iStart
= regExp
.RegFind(mainFile
, iStart
)) > -1)
196 int iLength
= regExp
.GetFindLen();
197 mainFile
= mainFile
.substr(0, iStart
) + regExp
.GetReplaceString(strRep
) + mainFile
.substr(iStart
+ iLength
);
201 CLog::Log(LOGINFO
, "{}: File matched:'{}' (RE='{}',Rep='{}') new filename:'{}'.",
202 __FUNCTION__
, strMatch
, strPat
, strRep
, mainFile
);
208 CLog::Log(LOGINFO
, "{}: Player : {}", __FUNCTION__
, m_filename
);
209 CLog::Log(LOGINFO
, "{}: File : {}", __FUNCTION__
, mainFile
);
210 CLog::Log(LOGINFO
, "{}: Content: {}", __FUNCTION__
, archiveContent
);
211 CLog::Log(LOGINFO
, "{}: Args : {}", __FUNCTION__
, m_args
);
212 CLog::Log(LOGINFO
, "{}: Start", __FUNCTION__
);
214 // make sure we surround the arguments with quotes where necessary
215 std::string strFName
;
216 std::string strFArgs
;
217 #if defined(TARGET_WINDOWS_DESKTOP)
218 // W32 batch-file handline
219 if (StringUtils::EndsWith(m_filename
, ".bat") || StringUtils::EndsWith(m_filename
, ".cmd"))
221 // MSDN says you just need to do this, but cmd's handing of spaces and
222 // quotes is soo broken it seems to work much better if you just omit
223 // lpApplicationName and enclose the module in lpCommandLine in quotes
224 //strFName = "cmd.exe";
229 strFName
= m_filename
;
231 strFArgs
.append("\"");
232 strFArgs
.append(m_filename
);
233 strFArgs
.append("\" ");
234 strFArgs
.append(m_args
);
236 int nReplaced
= StringUtils::Replace(strFArgs
, "{0}", mainFile
);
239 nReplaced
= StringUtils::Replace(strFArgs
, "{1}", mainFile
) + StringUtils::Replace(strFArgs
, "{2}", archiveContent
);
243 strFArgs
.append(" \"");
244 strFArgs
.append(mainFile
);
245 strFArgs
.append("\"");
248 #if defined(TARGET_WINDOWS_DESKTOP)
251 GetCursorPos(&m_ptCursorpos
);
254 switch (m_warpcursor
)
256 case WARP_BOTTOM_RIGHT
:
257 x
= GetSystemMetrics(SM_CXSCREEN
);
258 case WARP_BOTTOM_LEFT
:
259 y
= GetSystemMetrics(SM_CYSCREEN
);
262 x
= GetSystemMetrics(SM_CXSCREEN
);
265 x
= GetSystemMetrics(SM_CXSCREEN
) / 2;
266 y
= GetSystemMetrics(SM_CYSCREEN
) / 2;
269 CLog::Log(LOGINFO
, "{}: Warping cursor to ({},{})", __FUNCTION__
, x
, y
);
273 LONG currentStyle
= GetWindowLong(g_hWnd
, GWL_EXSTYLE
);
276 if (m_hidexbmc
&& !m_islauncher
)
278 CLog::Log(LOGINFO
, "{}: Hiding {} window", __FUNCTION__
, CCompileInfo::GetAppName());
279 CServiceBroker::GetWinSystem()->Hide();
281 #if defined(TARGET_WINDOWS_DESKTOP)
282 else if (currentStyle
& WS_EX_TOPMOST
)
284 CLog::Log(LOGINFO
, "{}: Lowering {} window", __FUNCTION__
, CCompileInfo::GetAppName());
285 SetWindowPos(g_hWnd
, HWND_BOTTOM
, 0, 0, 0, 0, SWP_NOMOVE
| SWP_NOSIZE
| SWP_NOREDRAW
| SWP_ASYNCWINDOWPOS
);
288 CLog::Log(LOGDEBUG
, "{}: Unlocking foreground window", __FUNCTION__
);
289 LockSetForegroundWindow(LSFW_UNLOCK
);
292 m_playbackStartTime
= std::chrono::steady_clock::now();
294 /* Suspend AE temporarily so exclusive or hog-mode sinks */
295 /* don't block external player's access to audio device */
296 CServiceBroker::GetActiveAE()->Suspend();
297 // wait for AE has completed suspended
298 XbmcThreads::EndTime
<> timer(2000ms
);
299 while (!timer
.IsTimePast() && !CServiceBroker::GetActiveAE()->IsSuspended())
301 CThread::Sleep(50ms
);
303 if (timer
.IsTimePast())
305 CLog::Log(LOGERROR
, "{}: AudioEngine did not suspend before launching external player",
309 m_callback
.OnPlayBackStarted(m_file
);
310 m_callback
.OnAVStarted(m_file
);
313 #if defined(TARGET_WINDOWS_DESKTOP)
314 ret
= ExecuteAppW32(strFName
.c_str(),strFArgs
.c_str());
315 #elif defined(TARGET_ANDROID)
316 ret
= ExecuteAppAndroid(m_filename
.c_str(), mainFile
.c_str());
317 #elif defined(TARGET_POSIX) && !defined(TARGET_DARWIN_EMBEDDED)
318 ret
= ExecuteAppLinux(strFArgs
.c_str());
320 auto end
= std::chrono::steady_clock::now();
321 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- m_playbackStartTime
);
323 if (ret
&& (m_islauncher
|| duration
.count() < LAUNCHER_PROCESS_TIME
))
327 CLog::Log(LOGINFO
, "{}: {} cannot stay hidden for a launcher process", __FUNCTION__
,
328 CCompileInfo::GetAppName());
329 CServiceBroker::GetWinSystem()->Show(false);
333 m_dialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogOK
>(WINDOW_DIALOG_OK
);
334 m_dialog
->SetHeading(CVariant
{23100});
335 m_dialog
->SetLine(1, CVariant
{23104});
336 m_dialog
->SetLine(2, CVariant
{23105});
337 m_dialog
->SetLine(3, CVariant
{23106});
340 if (!m_bAbortRequest
)
344 m_bIsPlaying
= false;
345 CLog::Log(LOGINFO
, "{}: Stop", __FUNCTION__
);
347 #if defined(TARGET_WINDOWS_DESKTOP)
348 CServiceBroker::GetWinSystem()->Restore();
350 if (currentStyle
& WS_EX_TOPMOST
)
352 CLog::Log(LOGINFO
, "{}: Showing {} window TOPMOST", __FUNCTION__
, CCompileInfo::GetAppName());
353 SetWindowPos(g_hWnd
, HWND_TOPMOST
, 0, 0, 0, 0, SWP_NOMOVE
| SWP_NOSIZE
| SWP_SHOWWINDOW
| SWP_ASYNCWINDOWPOS
);
354 SetForegroundWindow(g_hWnd
);
359 CLog::Log(LOGINFO
, "{}: Showing {} window", __FUNCTION__
, CCompileInfo::GetAppName());
360 CServiceBroker::GetWinSystem()->Show();
363 #if defined(TARGET_WINDOWS_DESKTOP)
368 if (&m_ptCursorpos
!= 0)
370 m_xPos
= (m_ptCursorpos
.x
);
371 m_yPos
= (m_ptCursorpos
.y
);
373 CLog::Log(LOGINFO
, "{}: Restoring cursor to ({},{})", __FUNCTION__
, m_xPos
, m_yPos
);
374 SetCursorPos(m_xPos
,m_yPos
);
379 bookmark
.totalTimeInSeconds
= 1;
380 bookmark
.timeInSeconds
= (duration
.count() / 1000 >= m_playCountMinTime
) ? 1 : 0;
381 bookmark
.player
= m_name
;
382 m_callback
.OnPlayerCloseFile(m_file
, bookmark
);
384 /* Resume AE processing of XBMC native audio */
385 if (!CServiceBroker::GetActiveAE()->Resume())
387 CLog::Log(LOGFATAL
, "{}: Failed to restart AudioEngine after return from external player",
391 // We don't want to come back to an active screensaver
392 auto& components
= CServiceBroker::GetAppComponents();
393 const auto appPower
= components
.GetComponent
<CApplicationPowerHandling
>();
394 appPower
->ResetScreenSaver();
395 appPower
->WakeUpScreenSaverAndDPMS();
397 if (!ret
|| (m_playOneStackItem
&& g_application
.CurrentFileItem().IsStack()))
398 m_callback
.OnPlayBackStopped();
400 m_callback
.OnPlayBackEnded();
403 #if defined(TARGET_WINDOWS_DESKTOP)
404 bool CExternalPlayer::ExecuteAppW32(const char* strPath
, const char* strSwitches
)
406 CLog::Log(LOGINFO
, "{}: {} {}", __FUNCTION__
, strPath
, strSwitches
);
408 STARTUPINFOW si
= {};
410 si
.dwFlags
= STARTF_USESHOWWINDOW
;
411 si
.wShowWindow
= m_hideconsole
? SW_HIDE
: SW_SHOW
;
413 std::wstring WstrPath
, WstrSwitches
;
414 g_charsetConverter
.utf8ToW(strPath
, WstrPath
, false);
415 g_charsetConverter
.utf8ToW(strSwitches
, WstrSwitches
, false);
417 if (m_bAbortRequest
) return false;
419 BOOL ret
= CreateProcessW(WstrPath
.empty() ? NULL
: WstrPath
.c_str(),
420 (LPWSTR
) WstrSwitches
.c_str(), NULL
, NULL
, FALSE
, NULL
,
421 NULL
, NULL
, &si
, &m_processInfo
);
425 DWORD lastError
= GetLastError();
426 CLog::Log(LOGINFO
, "{} - Failure: {}", __FUNCTION__
, lastError
);
430 int res
= WaitForSingleObject(m_processInfo
.hProcess
, INFINITE
);
435 CLog::Log(LOGINFO
, "{}: WAIT_OBJECT_0", __FUNCTION__
);
438 CLog::Log(LOGINFO
, "{}: WAIT_ABANDONED", __FUNCTION__
);
441 CLog::Log(LOGINFO
, "{}: WAIT_TIMEOUT", __FUNCTION__
);
444 CLog::Log(LOGINFO
, "{}: WAIT_FAILED ({})", __FUNCTION__
, GetLastError());
449 CloseHandle(m_processInfo
.hThread
);
450 m_processInfo
.hThread
= 0;
451 CloseHandle(m_processInfo
.hProcess
);
452 m_processInfo
.hProcess
= 0;
454 return (ret
== TRUE
);
458 #if !defined(TARGET_ANDROID) && !defined(TARGET_DARWIN_EMBEDDED) && defined(TARGET_POSIX)
459 bool CExternalPlayer::ExecuteAppLinux(const char* strSwitches
)
461 CLog::Log(LOGINFO
, "{}: {}", __FUNCTION__
, strSwitches
);
463 int ret
= system(strSwitches
);
466 CLog::Log(LOGINFO
, "{}: Failure: {}", __FUNCTION__
, ret
);
473 #if defined(TARGET_ANDROID)
474 bool CExternalPlayer::ExecuteAppAndroid(const char* strSwitches
,const char* strPath
)
476 CLog::Log(LOGINFO
, "{}: {}", __FUNCTION__
, strSwitches
);
478 bool ret
= CXBMCApp::StartActivity(strSwitches
, "android.intent.action.VIEW", "video/*", strPath
);
482 CLog::Log(LOGINFO
, "{}: Failure", __FUNCTION__
);
489 void CExternalPlayer::Pause()
493 bool CExternalPlayer::HasVideo() const
498 bool CExternalPlayer::HasAudio() const
503 bool CExternalPlayer::CanSeek() const
508 void CExternalPlayer::Seek(bool bPlus
, bool bLargeStep
, bool bChapterOverride
)
512 void CExternalPlayer::SeekPercentage(float iPercent
)
516 void CExternalPlayer::SetAVDelay(float fValue
)
520 float CExternalPlayer::GetAVDelay()
525 void CExternalPlayer::SetSubTitleDelay(float fValue
)
529 float CExternalPlayer::GetSubTitleDelay()
534 void CExternalPlayer::SeekTime(int64_t iTime
)
538 void CExternalPlayer::SetSpeed(float speed
)
541 CDataCacheCore::GetInstance().SetSpeed(1.0, speed
);
544 bool CExternalPlayer::SetPlayerState(const std::string
& state
)
549 bool CExternalPlayer::Initialize(TiXmlElement
* pConfig
)
551 XMLUtils::GetString(pConfig
, "filename", m_filename
);
552 if (m_filename
.length() > 0)
554 CLog::Log(LOGINFO
, "ExternalPlayer Filename: {}", m_filename
);
560 CLog::Log(LOGERROR
, "ExternalPlayer Error: filename element missing from: {}", xml
);
564 XMLUtils::GetString(pConfig
, "args", m_args
);
565 XMLUtils::GetBoolean(pConfig
, "playonestackitem", m_playOneStackItem
);
566 XMLUtils::GetBoolean(pConfig
, "islauncher", m_islauncher
);
567 XMLUtils::GetBoolean(pConfig
, "hidexbmc", m_hidexbmc
);
568 if (!XMLUtils::GetBoolean(pConfig
, "hideconsole", m_hideconsole
))
570 #ifdef TARGET_WINDOWS_DESKTOP
571 // Default depends on whether player is a batch file
572 m_hideconsole
= StringUtils::EndsWith(m_filename
, ".bat");
577 if (XMLUtils::GetBoolean(pConfig
, "hidecursor", bHideCursor
) && bHideCursor
)
578 m_warpcursor
= WARP_BOTTOM_RIGHT
;
580 std::string warpCursor
;
581 if (XMLUtils::GetString(pConfig
, "warpcursor", warpCursor
) && !warpCursor
.empty())
583 if (warpCursor
== "bottomright") m_warpcursor
= WARP_BOTTOM_RIGHT
;
584 else if (warpCursor
== "bottomleft") m_warpcursor
= WARP_BOTTOM_LEFT
;
585 else if (warpCursor
== "topleft") m_warpcursor
= WARP_TOP_LEFT
;
586 else if (warpCursor
== "topright") m_warpcursor
= WARP_TOP_RIGHT
;
587 else if (warpCursor
== "center") m_warpcursor
= WARP_CENTER
;
591 CLog::Log(LOGWARNING
, "ExternalPlayer: invalid value for warpcursor: {}", warpCursor
);
595 XMLUtils::GetInt(pConfig
, "playcountminimumtime", m_playCountMinTime
, 1, INT_MAX
);
599 "ExternalPlayer Tweaks: hideconsole ({}), hidexbmc ({}), islauncher ({}), warpcursor ({})",
600 m_hideconsole
? "true" : "false", m_hidexbmc
? "true" : "false",
601 m_islauncher
? "true" : "false", warpCursor
);
603 #ifdef TARGET_WINDOWS_DESKTOP
604 m_filenameReplacers
.push_back("^smb:// , / , \\\\ , g");
605 m_filenameReplacers
.push_back("^smb:\\\\\\\\ , smb:(\\\\\\\\[^\\\\]*\\\\) , \\1 , ");
608 TiXmlElement
* pReplacers
= pConfig
->FirstChildElement("replacers");
611 GetCustomRegexpReplacers(pReplacers
, m_filenameReplacers
);
612 pReplacers
= pReplacers
->NextSiblingElement("replacers");
618 void CExternalPlayer::GetCustomRegexpReplacers(TiXmlElement
*pRootElement
,
619 std::vector
<std::string
>& settings
)
621 int iAction
= 0; // overwrite
622 // for backward compatibility
623 const char* szAppend
= pRootElement
->Attribute("append");
624 if ((szAppend
&& StringUtils::CompareNoCase(szAppend
, "yes") == 0))
626 // action takes precedence if both attributes exist
627 const char* szAction
= pRootElement
->Attribute("action");
630 iAction
= 0; // overwrite
631 if (StringUtils::CompareNoCase(szAction
, "append") == 0)
632 iAction
= 1; // append
633 else if (StringUtils::CompareNoCase(szAction
, "prepend") == 0)
634 iAction
= 2; // prepend
639 TiXmlElement
* pReplacer
= pRootElement
->FirstChildElement("replacer");
643 if (pReplacer
->FirstChild())
645 const char* szGlobal
= pReplacer
->Attribute("global");
646 const char* szStop
= pReplacer
->Attribute("stop");
647 bool bGlobal
= szGlobal
&& StringUtils::CompareNoCase(szGlobal
, "true") == 0;
648 bool bStop
= szStop
&& StringUtils::CompareNoCase(szStop
, "true") == 0;
650 std::string strMatch
;
653 XMLUtils::GetString(pReplacer
,"match",strMatch
);
654 XMLUtils::GetString(pReplacer
,"pat",strPat
);
655 XMLUtils::GetString(pReplacer
,"rep",strRep
);
657 if (!strPat
.empty() && !strRep
.empty())
659 CLog::Log(LOGDEBUG
," Registering replacer:");
660 CLog::Log(LOGDEBUG
, " Match:[{}] Pattern:[{}] Replacement:[{}]", strMatch
, strPat
,
662 CLog::Log(LOGDEBUG
, " Global:[{}] Stop:[{}]", bGlobal
? "true" : "false",
663 bStop
? "true" : "false");
664 // keep literal commas since we use comma as a separator
665 StringUtils::Replace(strMatch
, ",",",,");
666 StringUtils::Replace(strPat
, ",",",,");
667 StringUtils::Replace(strRep
, ",",",,");
669 std::string strReplacer
= strMatch
+ " , " + strPat
+ " , " + strRep
+ " , " + (bGlobal
? "g" : "") + (bStop
? "s" : "");
671 settings
.insert(settings
.begin() + i
++, 1, strReplacer
);
673 settings
.push_back(strReplacer
);
677 // error message about missing tag
679 CLog::Log(LOGERROR
," Missing <Pat> tag");
681 CLog::Log(LOGERROR
," Missing <Rep> tag");
685 pReplacer
= pReplacer
->NextSiblingElement("replacer");