2 * Copyright (C) 2005-2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
21 #include "threads/SystemClock.h"
25 #include "threads/SingleLock.h"
26 #include "ExternalPlayer.h"
27 #include "windowing/WindowingFactory.h"
28 #include "dialogs/GUIDialogOK.h"
29 #include "guilib/GUIWindowManager.h"
30 #include "Application.h"
31 #include "filesystem/MusicDatabaseFile.h"
33 #include "utils/RegExp.h"
34 #include "utils/StringUtils.h"
35 #include "utils/URIUtils.h"
37 #include "utils/XMLUtils.h"
38 #include "utils/TimeUtils.h"
39 #include "utils/log.h"
40 #include "cores/AudioEngine/AEFactory.h"
41 #if defined(TARGET_WINDOWS)
42 #include "utils/CharsetConverter.h"
44 #ifdef HAS_IRSERVERSUITE
45 #include "input/windows/IRServerSuite.h"
49 #include "input/linux/LIRC.h"
51 #if defined(TARGET_ANDROID)
52 #include "android/activity/XBMCApp.h"
55 // If the process ends in less than this time (ms), we assume it's a launcher
56 // and wait for manual intervention before continuing
57 #define LAUNCHER_PROCESS_TIME 2000
58 // Time (ms) we give a process we sent a WM_QUIT to close before terminating
59 #define PROCESS_GRACE_TIME 3000
60 // Default time after which the item's playcount is incremented
61 #define DEFAULT_PLAYCOUNT_MIN_TIME 10
63 using namespace XFILE;
65 #if defined(TARGET_WINDOWS)
69 CExternalPlayer::CExternalPlayer(IPlayerCallback& callback)
71 CThread("ExternalPlayer")
73 m_bAbortRequest = false;
76 m_playbackStartTime = 0;
81 m_hideconsole = false;
82 m_warpcursor = WARP_NONE;
85 m_playCountMinTime = DEFAULT_PLAYCOUNT_MIN_TIME;
86 m_playOneStackItem = false;
94 #if defined(TARGET_WINDOWS)
95 memset(&m_processInfo, 0, sizeof(m_processInfo));
99 CExternalPlayer::~CExternalPlayer()
104 bool CExternalPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options)
109 m_launchFilename = file.GetPath();
110 CLog::Log(LOGNOTICE, "%s: %s", __FUNCTION__, m_launchFilename.c_str());
117 m_bIsPlaying = false;
118 CLog::Log(LOGERROR,"%s - Exception thrown", __FUNCTION__);
123 bool CExternalPlayer::CloseFile(bool reopen)
125 m_bAbortRequest = true;
127 if (m_dialog && m_dialog->IsActive()) m_dialog->Close();
129 #if defined(TARGET_WINDOWS)
130 if (m_bIsPlaying && m_processInfo.hProcess)
132 TerminateProcess(m_processInfo.hProcess, 1);
139 bool CExternalPlayer::IsPlaying() const
144 void CExternalPlayer::Process()
146 CStdString mainFile = m_launchFilename;
147 CStdString archiveContent = "";
149 if (m_args.find("{0}") == std::string::npos)
151 // Unwind archive names
152 CURL url(m_launchFilename);
153 CStdString protocol = url.GetProtocol();
154 if (protocol == "zip" || protocol == "rar"/* || protocol == "iso9660" ??*/ || protocol == "udf")
156 mainFile = url.GetHostName();
157 archiveContent = url.GetFileName();
159 if (protocol == "musicdb")
160 mainFile = CMusicDatabaseFile::TranslateUrl(url);
161 if (protocol == "bluray")
163 CURL base(url.GetHostName());
164 if(base.GetProtocol() == "udf")
166 mainFile = base.GetHostName(); /* image file */
167 archiveContent = base.GetFileName();
170 mainFile = URIUtils::AddFileToFolder(base.Get(), url.GetFileName());
174 if (m_filenameReplacers.size() > 0)
176 for (unsigned int i = 0; i < m_filenameReplacers.size(); i++)
178 std::vector<CStdString> vecSplit;
179 StringUtils::SplitString(m_filenameReplacers[i], " , ", vecSplit);
181 // something is wrong, go to next substitution
182 if (vecSplit.size() != 4)
185 CStdString strMatch = vecSplit[0];
186 StringUtils::Replace(strMatch, ",,",",");
187 bool bCaseless = vecSplit[3].find('i') != std::string::npos;
188 CRegExp regExp(bCaseless, CRegExp::autoUtf8);
190 if (!regExp.RegComp(strMatch.c_str()))
191 { // invalid regexp - complain in logs
192 CLog::Log(LOGERROR, "%s: Invalid RegExp:'%s'", __FUNCTION__, strMatch.c_str());
196 if (regExp.RegFind(mainFile) > -1)
198 CStdString strPat = vecSplit[1];
199 StringUtils::Replace(strPat, ",,",",");
201 if (!regExp.RegComp(strPat.c_str()))
202 { // invalid regexp - complain in logs
203 CLog::Log(LOGERROR, "%s: Invalid RegExp:'%s'", __FUNCTION__, strPat.c_str());
207 CStdString strRep = vecSplit[2];
208 StringUtils::Replace(strRep, ",,",",");
209 bool bGlobal = vecSplit[3].find('g') != std::string::npos;
210 bool bStop = vecSplit[3].find('s') != std::string::npos;
212 while ((iStart = regExp.RegFind(mainFile, iStart)) > -1)
214 int iLength = regExp.GetFindLen();
215 mainFile = mainFile.substr(0, iStart) + regExp.GetReplaceString(strRep) + mainFile.substr(iStart + iLength);
219 CLog::Log(LOGINFO, "%s: File matched:'%s' (RE='%s',Rep='%s') new filename:'%s'.", __FUNCTION__, strMatch.c_str(), strPat.c_str(), strRep.c_str(), mainFile.c_str());
225 CLog::Log(LOGNOTICE, "%s: Player : %s", __FUNCTION__, m_filename.c_str());
226 CLog::Log(LOGNOTICE, "%s: File : %s", __FUNCTION__, mainFile.c_str());
227 CLog::Log(LOGNOTICE, "%s: Content: %s", __FUNCTION__, archiveContent.c_str());
228 CLog::Log(LOGNOTICE, "%s: Args : %s", __FUNCTION__, m_args.c_str());
229 CLog::Log(LOGNOTICE, "%s: Start", __FUNCTION__);
231 // make sure we surround the arguments with quotes where necessary
234 #if defined(TARGET_WINDOWS)
235 // W32 batch-file handline
236 if (StringUtils::EndsWith(m_filename, ".bat") || StringUtils::EndsWith(m_filename, ".cmd"))
238 // MSDN says you just need to do this, but cmd's handing of spaces and
239 // quotes is soo broken it seems to work much better if you just omit
240 // lpApplicationName and enclose the module in lpCommandLine in quotes
241 //strFName = "cmd.exe";
246 strFName = m_filename;
248 strFArgs.append("\"");
249 strFArgs.append(m_filename);
250 strFArgs.append("\" ");
251 strFArgs.append(m_args);
253 int nReplaced = StringUtils::Replace(strFArgs, "{0}", mainFile);
256 nReplaced = StringUtils::Replace(strFArgs, "{1}", mainFile) + StringUtils::Replace(strFArgs, "{2}", archiveContent);
260 strFArgs.append(" \"");
261 strFArgs.append(mainFile);
262 strFArgs.append("\"");
265 #if defined(TARGET_WINDOWS)
268 GetCursorPos(&m_ptCursorpos);
271 switch (m_warpcursor)
273 case WARP_BOTTOM_RIGHT:
274 x = GetSystemMetrics(SM_CXSCREEN);
275 case WARP_BOTTOM_LEFT:
276 y = GetSystemMetrics(SM_CYSCREEN);
279 x = GetSystemMetrics(SM_CXSCREEN);
282 x = GetSystemMetrics(SM_CXSCREEN) / 2;
283 y = GetSystemMetrics(SM_CYSCREEN) / 2;
286 CLog::Log(LOGNOTICE, "%s: Warping cursor to (%d,%d)", __FUNCTION__, x, y);
290 LONG currentStyle = GetWindowLong(g_hWnd, GWL_EXSTYLE);
293 if (m_hidexbmc && !m_islauncher)
295 CLog::Log(LOGNOTICE, "%s: Hiding XBMC window", __FUNCTION__);
298 #if defined(TARGET_WINDOWS)
299 else if (currentStyle & WS_EX_TOPMOST)
301 CLog::Log(LOGNOTICE, "%s: Lowering XBMC window", __FUNCTION__);
302 SetWindowPos(g_hWnd,HWND_BOTTOM,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_NOREDRAW);
305 CLog::Log(LOGDEBUG, "%s: Unlocking foreground window", __FUNCTION__);
306 LockSetForegroundWindow(LSFW_UNLOCK);
309 m_playbackStartTime = XbmcThreads::SystemClockMillis();
311 /* Suspend AE temporarily so exclusive or hog-mode sinks */
312 /* don't block external player's access to audio device */
313 CAEFactory::Suspend();
314 // wait for AE has completed suspended
315 XbmcThreads::EndTime timer(2000);
316 while (!timer.IsTimePast() && !CAEFactory::IsSuspended())
320 if (timer.IsTimePast())
322 CLog::Log(LOGERROR,"%s: AudioEngine did not suspend before launching external player", __FUNCTION__);
326 #if defined(TARGET_WINDOWS)
327 ret = ExecuteAppW32(strFName.c_str(),strFArgs.c_str());
328 #elif defined(TARGET_ANDROID)
329 ret = ExecuteAppAndroid(m_filename.c_str(), mainFile.c_str());
330 #elif defined(TARGET_POSIX) || defined(TARGET_DARWIN_OSX)
331 ret = ExecuteAppLinux(strFArgs.c_str());
333 int64_t elapsedMillis = XbmcThreads::SystemClockMillis() - m_playbackStartTime;
335 if (ret && (m_islauncher || elapsedMillis < LAUNCHER_PROCESS_TIME))
339 CLog::Log(LOGNOTICE, "%s: XBMC cannot stay hidden for a launcher process", __FUNCTION__);
340 g_Windowing.Show(false);
344 CSingleLock lock(g_graphicsContext);
345 m_dialog = (CGUIDialogOK *)g_windowManager.GetWindow(WINDOW_DIALOG_OK);
346 m_dialog->SetHeading(23100);
347 m_dialog->SetLine(1, 23104);
348 m_dialog->SetLine(2, 23105);
349 m_dialog->SetLine(3, 23106);
352 if (!m_bAbortRequest) m_dialog->DoModal();
355 m_bIsPlaying = false;
356 CLog::Log(LOGNOTICE, "%s: Stop", __FUNCTION__);
358 #if defined(TARGET_WINDOWS)
359 g_Windowing.Restore();
361 if (currentStyle & WS_EX_TOPMOST)
363 CLog::Log(LOGNOTICE, "%s: Showing XBMC window TOPMOST", __FUNCTION__);
364 SetWindowPos(g_hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW);
365 SetForegroundWindow(g_hWnd);
370 CLog::Log(LOGNOTICE, "%s: Showing XBMC window", __FUNCTION__);
374 #if defined(TARGET_WINDOWS)
379 if (&m_ptCursorpos != 0)
381 m_xPos = (m_ptCursorpos.x);
382 m_yPos = (m_ptCursorpos.y);
384 CLog::Log(LOGNOTICE, "%s: Restoring cursor to (%d,%d)", __FUNCTION__, m_xPos, m_yPos);
385 SetCursorPos(m_xPos,m_yPos);
389 /* Resume AE processing of XBMC native audio */
390 if (!CAEFactory::Resume())
392 CLog::Log(LOGFATAL, "%s: Failed to restart AudioEngine after return from external player",__FUNCTION__);
395 // We don't want to come back to an active screensaver
396 g_application.ResetScreenSaver();
397 g_application.WakeUpScreenSaverAndDPMS();
399 if (!ret || (m_playOneStackItem && g_application.CurrentFileItem().IsStack()))
400 m_callback.OnPlayBackStopped();
402 m_callback.OnPlayBackEnded();
405 #if defined(TARGET_WINDOWS)
406 BOOL CExternalPlayer::ExecuteAppW32(const char* strPath, const char* strSwitches)
408 CLog::Log(LOGNOTICE, "%s: %s %s", __FUNCTION__, strPath, strSwitches);
411 memset(&si, 0, sizeof(si));
413 si.dwFlags = STARTF_USESHOWWINDOW;
414 si.wShowWindow = m_hideconsole ? SW_HIDE : SW_SHOW;
416 CStdStringW WstrPath, WstrSwitches;
417 g_charsetConverter.utf8ToW(strPath, WstrPath);
418 g_charsetConverter.utf8ToW(strSwitches, WstrSwitches);
420 if (m_bAbortRequest) return false;
422 BOOL ret = CreateProcessW(WstrPath.empty() ? NULL : WstrPath.c_str(),
423 (LPWSTR) WstrSwitches.c_str(), NULL, NULL, FALSE, NULL,
424 NULL, NULL, &si, &m_processInfo);
428 DWORD lastError = GetLastError();
429 CLog::Log(LOGNOTICE, "%s - Failure: %d", __FUNCTION__, lastError);
433 int res = WaitForSingleObject(m_processInfo.hProcess, INFINITE);
438 CLog::Log(LOGNOTICE, "%s: WAIT_OBJECT_0", __FUNCTION__);
441 CLog::Log(LOGNOTICE, "%s: WAIT_ABANDONED", __FUNCTION__);
444 CLog::Log(LOGNOTICE, "%s: WAIT_TIMEOUT", __FUNCTION__);
447 CLog::Log(LOGNOTICE, "%s: WAIT_FAILED (%d)", __FUNCTION__, GetLastError());
452 CloseHandle(m_processInfo.hThread);
453 m_processInfo.hThread = 0;
454 CloseHandle(m_processInfo.hProcess);
455 m_processInfo.hProcess = 0;
462 #if !defined(TARGET_ANDROID) && (defined(TARGET_POSIX) || defined(TARGET_DARWIN_OSX))
463 BOOL CExternalPlayer::ExecuteAppLinux(const char* strSwitches)
465 CLog::Log(LOGNOTICE, "%s: %s", __FUNCTION__, strSwitches);
467 bool remoteused = g_RemoteControl.IsInUse();
468 g_RemoteControl.Disconnect();
469 g_RemoteControl.setUsed(false);
472 int ret = system(strSwitches);
475 g_RemoteControl.setUsed(remoteused);
476 g_RemoteControl.Initialize();
481 CLog::Log(LOGNOTICE, "%s: Failure: %d", __FUNCTION__, ret);
488 #if defined(TARGET_ANDROID)
489 BOOL CExternalPlayer::ExecuteAppAndroid(const char* strSwitches,const char* strPath)
491 CLog::Log(LOGNOTICE, "%s: %s", __FUNCTION__, strSwitches);
493 int ret = CXBMCApp::StartActivity(strSwitches, "android.intent.action.VIEW", "video/*", strPath);
497 CLog::Log(LOGNOTICE, "%s: Failure: %d", __FUNCTION__, ret);
504 void CExternalPlayer::Pause()
508 bool CExternalPlayer::IsPaused() const
513 bool CExternalPlayer::HasVideo() const
518 bool CExternalPlayer::HasAudio() const
523 void CExternalPlayer::SwitchToNextLanguage()
527 void CExternalPlayer::ToggleSubtitles()
531 bool CExternalPlayer::CanSeek()
536 void CExternalPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
540 void CExternalPlayer::GetAudioInfo(CStdString& strAudioInfo)
542 strAudioInfo = "CExternalPlayer:GetAudioInfo";
545 void CExternalPlayer::GetVideoInfo(CStdString& strVideoInfo)
547 strVideoInfo = "CExternalPlayer:GetVideoInfo";
550 void CExternalPlayer::GetGeneralInfo(CStdString& strGeneralInfo)
552 strGeneralInfo = "CExternalPlayer:GetGeneralInfo";
555 void CExternalPlayer::SwitchToNextAudioLanguage()
559 void CExternalPlayer::SeekPercentage(float iPercent)
563 float CExternalPlayer::GetPercentage()
565 int64_t iTime = GetTime();
566 int64_t iTotalTime = GetTotalTime();
570 CLog::Log(LOGDEBUG, "Percentage is %f", (iTime * 100 / (float)iTotalTime));
571 return iTime * 100 / (float)iTotalTime;
577 void CExternalPlayer::SetAVDelay(float fValue)
581 float CExternalPlayer::GetAVDelay()
586 void CExternalPlayer::SetSubTitleDelay(float fValue)
590 float CExternalPlayer::GetSubTitleDelay()
595 void CExternalPlayer::SeekTime(int64_t iTime)
599 int64_t CExternalPlayer::GetTime() // in millis
601 if ((XbmcThreads::SystemClockMillis() - m_playbackStartTime) / 1000 > m_playCountMinTime)
603 m_time = m_totalTime * 1000;
609 int64_t CExternalPlayer::GetTotalTime() // in milliseconds
611 return (int64_t)m_totalTime * 1000;
614 void CExternalPlayer::ToFFRW(int iSpeed)
619 void CExternalPlayer::ShowOSD(bool bOnoff)
623 CStdString CExternalPlayer::GetPlayerState()
628 bool CExternalPlayer::SetPlayerState(CStdString state)
633 bool CExternalPlayer::Initialize(TiXmlElement* pConfig)
635 XMLUtils::GetString(pConfig, "filename", m_filename);
636 if (m_filename.length() > 0)
638 CLog::Log(LOGNOTICE, "ExternalPlayer Filename: %s", m_filename.c_str());
644 CLog::Log(LOGERROR, "ExternalPlayer Error: filename element missing from: %s", xml.c_str());
648 XMLUtils::GetString(pConfig, "args", m_args);
649 XMLUtils::GetBoolean(pConfig, "playonestackitem", m_playOneStackItem);
650 XMLUtils::GetBoolean(pConfig, "islauncher", m_islauncher);
651 XMLUtils::GetBoolean(pConfig, "hidexbmc", m_hidexbmc);
652 if (!XMLUtils::GetBoolean(pConfig, "hideconsole", m_hideconsole))
654 #ifdef TARGET_WINDOWS
655 // Default depends on whether player is a batch file
656 m_hideconsole = StringUtils::EndsWith(m_filename, ".bat");
661 if (XMLUtils::GetBoolean(pConfig, "hidecursor", bHideCursor) && bHideCursor)
662 m_warpcursor = WARP_BOTTOM_RIGHT;
664 CStdString warpCursor;
665 if (XMLUtils::GetString(pConfig, "warpcursor", warpCursor) && !warpCursor.empty())
667 if (warpCursor == "bottomright") m_warpcursor = WARP_BOTTOM_RIGHT;
668 else if (warpCursor == "bottomleft") m_warpcursor = WARP_BOTTOM_LEFT;
669 else if (warpCursor == "topleft") m_warpcursor = WARP_TOP_LEFT;
670 else if (warpCursor == "topright") m_warpcursor = WARP_TOP_RIGHT;
671 else if (warpCursor == "center") m_warpcursor = WARP_CENTER;
675 CLog::Log(LOGWARNING, "ExternalPlayer: invalid value for warpcursor: %s", warpCursor.c_str());
679 XMLUtils::GetInt(pConfig, "playcountminimumtime", m_playCountMinTime, 1, INT_MAX);
681 CLog::Log(LOGNOTICE, "ExternalPlayer Tweaks: hideconsole (%s), hidexbmc (%s), islauncher (%s), warpcursor (%s)",
682 m_hideconsole ? "true" : "false",
683 m_hidexbmc ? "true" : "false",
684 m_islauncher ? "true" : "false",
687 #ifdef TARGET_WINDOWS
688 m_filenameReplacers.push_back("^smb:// , / , \\\\ , g");
689 m_filenameReplacers.push_back("^smb:\\\\\\\\ , smb:(\\\\\\\\[^\\\\]*\\\\) , \\1 , ");
692 TiXmlElement* pReplacers = pConfig->FirstChildElement("replacers");
695 GetCustomRegexpReplacers(pReplacers, m_filenameReplacers);
696 pReplacers = pReplacers->NextSiblingElement("replacers");
702 void CExternalPlayer::GetCustomRegexpReplacers(TiXmlElement *pRootElement,
703 CStdStringArray& settings)
705 int iAction = 0; // overwrite
706 // for backward compatibility
707 const char* szAppend = pRootElement->Attribute("append");
708 if ((szAppend && stricmp(szAppend, "yes") == 0))
710 // action takes precedence if both attributes exist
711 const char* szAction = pRootElement->Attribute("action");
714 iAction = 0; // overwrite
715 if (stricmp(szAction, "append") == 0)
716 iAction = 1; // append
717 else if (stricmp(szAction, "prepend") == 0)
718 iAction = 2; // prepend
723 TiXmlElement* pReplacer = pRootElement->FirstChildElement("replacer");
727 if (pReplacer->FirstChild())
729 const char* szGlobal = pReplacer->Attribute("global");
730 const char* szStop = pReplacer->Attribute("stop");
731 bool bGlobal = szGlobal && stricmp(szGlobal, "true") == 0;
732 bool bStop = szStop && stricmp(szStop, "true") == 0;
737 XMLUtils::GetString(pReplacer,"match",strMatch);
738 XMLUtils::GetString(pReplacer,"pat",strPat);
739 XMLUtils::GetString(pReplacer,"rep",strRep);
741 if (!strPat.empty() && !strRep.empty())
743 CLog::Log(LOGDEBUG," Registering replacer:");
744 CLog::Log(LOGDEBUG," Match:[%s] Pattern:[%s] Replacement:[%s]", strMatch.c_str(), strPat.c_str(), strRep.c_str());
745 CLog::Log(LOGDEBUG," Global:[%s] Stop:[%s]", bGlobal?"true":"false", bStop?"true":"false");
746 // keep literal commas since we use comma as a seperator
747 StringUtils::Replace(strMatch, ",",",,");
748 StringUtils::Replace(strPat, ",",",,");
749 StringUtils::Replace(strRep, ",",",,");
751 CStdString strReplacer = strMatch + " , " + strPat + " , " + strRep + " , " + (bGlobal ? "g" : "") + (bStop ? "s" : "");
753 settings.insert(settings.begin() + i++, 1, strReplacer);
755 settings.push_back(strReplacer);
759 // error message about missing tag
761 CLog::Log(LOGERROR," Missing <Pat> tag");
763 CLog::Log(LOGERROR," Missing <Rep> tag");
767 pReplacer = pReplacer->NextSiblingElement("replacer");