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__);
325 m_callback.OnPlayBackStarted();
328 #if defined(TARGET_WINDOWS)
329 ret = ExecuteAppW32(strFName.c_str(),strFArgs.c_str());
330 #elif defined(TARGET_ANDROID)
331 ret = ExecuteAppAndroid(m_filename.c_str(), mainFile.c_str());
332 #elif defined(TARGET_POSIX) || defined(TARGET_DARWIN_OSX)
333 ret = ExecuteAppLinux(strFArgs.c_str());
335 int64_t elapsedMillis = XbmcThreads::SystemClockMillis() - m_playbackStartTime;
337 if (ret && (m_islauncher || elapsedMillis < LAUNCHER_PROCESS_TIME))
341 CLog::Log(LOGNOTICE, "%s: XBMC cannot stay hidden for a launcher process", __FUNCTION__);
342 g_Windowing.Show(false);
346 CSingleLock lock(g_graphicsContext);
347 m_dialog = (CGUIDialogOK *)g_windowManager.GetWindow(WINDOW_DIALOG_OK);
348 m_dialog->SetHeading(23100);
349 m_dialog->SetLine(1, 23104);
350 m_dialog->SetLine(2, 23105);
351 m_dialog->SetLine(3, 23106);
354 if (!m_bAbortRequest) m_dialog->DoModal();
357 m_bIsPlaying = false;
358 CLog::Log(LOGNOTICE, "%s: Stop", __FUNCTION__);
360 #if defined(TARGET_WINDOWS)
361 g_Windowing.Restore();
363 if (currentStyle & WS_EX_TOPMOST)
365 CLog::Log(LOGNOTICE, "%s: Showing XBMC window TOPMOST", __FUNCTION__);
366 SetWindowPos(g_hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW);
367 SetForegroundWindow(g_hWnd);
372 CLog::Log(LOGNOTICE, "%s: Showing XBMC window", __FUNCTION__);
376 #if defined(TARGET_WINDOWS)
381 if (&m_ptCursorpos != 0)
383 m_xPos = (m_ptCursorpos.x);
384 m_yPos = (m_ptCursorpos.y);
386 CLog::Log(LOGNOTICE, "%s: Restoring cursor to (%d,%d)", __FUNCTION__, m_xPos, m_yPos);
387 SetCursorPos(m_xPos,m_yPos);
391 /* Resume AE processing of XBMC native audio */
392 if (!CAEFactory::Resume())
394 CLog::Log(LOGFATAL, "%s: Failed to restart AudioEngine after return from external player",__FUNCTION__);
397 // We don't want to come back to an active screensaver
398 g_application.ResetScreenSaver();
399 g_application.WakeUpScreenSaverAndDPMS();
401 if (!ret || (m_playOneStackItem && g_application.CurrentFileItem().IsStack()))
402 m_callback.OnPlayBackStopped();
404 m_callback.OnPlayBackEnded();
407 #if defined(TARGET_WINDOWS)
408 BOOL CExternalPlayer::ExecuteAppW32(const char* strPath, const char* strSwitches)
410 CLog::Log(LOGNOTICE, "%s: %s %s", __FUNCTION__, strPath, strSwitches);
413 memset(&si, 0, sizeof(si));
415 si.dwFlags = STARTF_USESHOWWINDOW;
416 si.wShowWindow = m_hideconsole ? SW_HIDE : SW_SHOW;
418 CStdStringW WstrPath, WstrSwitches;
419 g_charsetConverter.utf8ToW(strPath, WstrPath);
420 g_charsetConverter.utf8ToW(strSwitches, WstrSwitches);
422 if (m_bAbortRequest) return false;
424 BOOL ret = CreateProcessW(WstrPath.empty() ? NULL : WstrPath.c_str(),
425 (LPWSTR) WstrSwitches.c_str(), NULL, NULL, FALSE, NULL,
426 NULL, NULL, &si, &m_processInfo);
430 DWORD lastError = GetLastError();
431 CLog::Log(LOGNOTICE, "%s - Failure: %d", __FUNCTION__, lastError);
435 int res = WaitForSingleObject(m_processInfo.hProcess, INFINITE);
440 CLog::Log(LOGNOTICE, "%s: WAIT_OBJECT_0", __FUNCTION__);
443 CLog::Log(LOGNOTICE, "%s: WAIT_ABANDONED", __FUNCTION__);
446 CLog::Log(LOGNOTICE, "%s: WAIT_TIMEOUT", __FUNCTION__);
449 CLog::Log(LOGNOTICE, "%s: WAIT_FAILED (%d)", __FUNCTION__, GetLastError());
454 CloseHandle(m_processInfo.hThread);
455 m_processInfo.hThread = 0;
456 CloseHandle(m_processInfo.hProcess);
457 m_processInfo.hProcess = 0;
464 #if !defined(TARGET_ANDROID) && (defined(TARGET_POSIX) || defined(TARGET_DARWIN_OSX))
465 BOOL CExternalPlayer::ExecuteAppLinux(const char* strSwitches)
467 CLog::Log(LOGNOTICE, "%s: %s", __FUNCTION__, strSwitches);
469 bool remoteused = g_RemoteControl.IsInUse();
470 g_RemoteControl.Disconnect();
471 g_RemoteControl.setUsed(false);
474 int ret = system(strSwitches);
477 g_RemoteControl.setUsed(remoteused);
478 g_RemoteControl.Initialize();
483 CLog::Log(LOGNOTICE, "%s: Failure: %d", __FUNCTION__, ret);
490 #if defined(TARGET_ANDROID)
491 BOOL CExternalPlayer::ExecuteAppAndroid(const char* strSwitches,const char* strPath)
493 CLog::Log(LOGNOTICE, "%s: %s", __FUNCTION__, strSwitches);
495 int ret = CXBMCApp::StartActivity(strSwitches, "android.intent.action.VIEW", "video/*", strPath);
499 CLog::Log(LOGNOTICE, "%s: Failure: %d", __FUNCTION__, ret);
506 void CExternalPlayer::Pause()
510 bool CExternalPlayer::IsPaused() const
515 bool CExternalPlayer::HasVideo() const
520 bool CExternalPlayer::HasAudio() const
525 void CExternalPlayer::SwitchToNextLanguage()
529 void CExternalPlayer::ToggleSubtitles()
533 bool CExternalPlayer::CanSeek()
538 void CExternalPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
542 void CExternalPlayer::GetAudioInfo(CStdString& strAudioInfo)
544 strAudioInfo = "CExternalPlayer:GetAudioInfo";
547 void CExternalPlayer::GetVideoInfo(CStdString& strVideoInfo)
549 strVideoInfo = "CExternalPlayer:GetVideoInfo";
552 void CExternalPlayer::GetGeneralInfo(CStdString& strGeneralInfo)
554 strGeneralInfo = "CExternalPlayer:GetGeneralInfo";
557 void CExternalPlayer::SwitchToNextAudioLanguage()
561 void CExternalPlayer::SeekPercentage(float iPercent)
565 float CExternalPlayer::GetPercentage()
567 int64_t iTime = GetTime();
568 int64_t iTotalTime = GetTotalTime();
572 CLog::Log(LOGDEBUG, "Percentage is %f", (iTime * 100 / (float)iTotalTime));
573 return iTime * 100 / (float)iTotalTime;
579 void CExternalPlayer::SetAVDelay(float fValue)
583 float CExternalPlayer::GetAVDelay()
588 void CExternalPlayer::SetSubTitleDelay(float fValue)
592 float CExternalPlayer::GetSubTitleDelay()
597 void CExternalPlayer::SeekTime(int64_t iTime)
601 int64_t CExternalPlayer::GetTime() // in millis
603 if ((XbmcThreads::SystemClockMillis() - m_playbackStartTime) / 1000 > m_playCountMinTime)
605 m_time = m_totalTime * 1000;
611 int64_t CExternalPlayer::GetTotalTime() // in milliseconds
613 return (int64_t)m_totalTime * 1000;
616 void CExternalPlayer::ToFFRW(int iSpeed)
621 void CExternalPlayer::ShowOSD(bool bOnoff)
625 CStdString CExternalPlayer::GetPlayerState()
630 bool CExternalPlayer::SetPlayerState(CStdString state)
635 bool CExternalPlayer::Initialize(TiXmlElement* pConfig)
637 XMLUtils::GetString(pConfig, "filename", m_filename);
638 if (m_filename.length() > 0)
640 CLog::Log(LOGNOTICE, "ExternalPlayer Filename: %s", m_filename.c_str());
646 CLog::Log(LOGERROR, "ExternalPlayer Error: filename element missing from: %s", xml.c_str());
650 XMLUtils::GetString(pConfig, "args", m_args);
651 XMLUtils::GetBoolean(pConfig, "playonestackitem", m_playOneStackItem);
652 XMLUtils::GetBoolean(pConfig, "islauncher", m_islauncher);
653 XMLUtils::GetBoolean(pConfig, "hidexbmc", m_hidexbmc);
654 if (!XMLUtils::GetBoolean(pConfig, "hideconsole", m_hideconsole))
656 #ifdef TARGET_WINDOWS
657 // Default depends on whether player is a batch file
658 m_hideconsole = StringUtils::EndsWith(m_filename, ".bat");
663 if (XMLUtils::GetBoolean(pConfig, "hidecursor", bHideCursor) && bHideCursor)
664 m_warpcursor = WARP_BOTTOM_RIGHT;
666 CStdString warpCursor;
667 if (XMLUtils::GetString(pConfig, "warpcursor", warpCursor) && !warpCursor.empty())
669 if (warpCursor == "bottomright") m_warpcursor = WARP_BOTTOM_RIGHT;
670 else if (warpCursor == "bottomleft") m_warpcursor = WARP_BOTTOM_LEFT;
671 else if (warpCursor == "topleft") m_warpcursor = WARP_TOP_LEFT;
672 else if (warpCursor == "topright") m_warpcursor = WARP_TOP_RIGHT;
673 else if (warpCursor == "center") m_warpcursor = WARP_CENTER;
677 CLog::Log(LOGWARNING, "ExternalPlayer: invalid value for warpcursor: %s", warpCursor.c_str());
681 XMLUtils::GetInt(pConfig, "playcountminimumtime", m_playCountMinTime, 1, INT_MAX);
683 CLog::Log(LOGNOTICE, "ExternalPlayer Tweaks: hideconsole (%s), hidexbmc (%s), islauncher (%s), warpcursor (%s)",
684 m_hideconsole ? "true" : "false",
685 m_hidexbmc ? "true" : "false",
686 m_islauncher ? "true" : "false",
689 #ifdef TARGET_WINDOWS
690 m_filenameReplacers.push_back("^smb:// , / , \\\\ , g");
691 m_filenameReplacers.push_back("^smb:\\\\\\\\ , smb:(\\\\\\\\[^\\\\]*\\\\) , \\1 , ");
694 TiXmlElement* pReplacers = pConfig->FirstChildElement("replacers");
697 GetCustomRegexpReplacers(pReplacers, m_filenameReplacers);
698 pReplacers = pReplacers->NextSiblingElement("replacers");
704 void CExternalPlayer::GetCustomRegexpReplacers(TiXmlElement *pRootElement,
705 CStdStringArray& settings)
707 int iAction = 0; // overwrite
708 // for backward compatibility
709 const char* szAppend = pRootElement->Attribute("append");
710 if ((szAppend && stricmp(szAppend, "yes") == 0))
712 // action takes precedence if both attributes exist
713 const char* szAction = pRootElement->Attribute("action");
716 iAction = 0; // overwrite
717 if (stricmp(szAction, "append") == 0)
718 iAction = 1; // append
719 else if (stricmp(szAction, "prepend") == 0)
720 iAction = 2; // prepend
725 TiXmlElement* pReplacer = pRootElement->FirstChildElement("replacer");
729 if (pReplacer->FirstChild())
731 const char* szGlobal = pReplacer->Attribute("global");
732 const char* szStop = pReplacer->Attribute("stop");
733 bool bGlobal = szGlobal && stricmp(szGlobal, "true") == 0;
734 bool bStop = szStop && stricmp(szStop, "true") == 0;
739 XMLUtils::GetString(pReplacer,"match",strMatch);
740 XMLUtils::GetString(pReplacer,"pat",strPat);
741 XMLUtils::GetString(pReplacer,"rep",strRep);
743 if (!strPat.empty() && !strRep.empty())
745 CLog::Log(LOGDEBUG," Registering replacer:");
746 CLog::Log(LOGDEBUG," Match:[%s] Pattern:[%s] Replacement:[%s]", strMatch.c_str(), strPat.c_str(), strRep.c_str());
747 CLog::Log(LOGDEBUG," Global:[%s] Stop:[%s]", bGlobal?"true":"false", bStop?"true":"false");
748 // keep literal commas since we use comma as a seperator
749 StringUtils::Replace(strMatch, ",",",,");
750 StringUtils::Replace(strPat, ",",",,");
751 StringUtils::Replace(strRep, ",",",,");
753 CStdString strReplacer = strMatch + " , " + strPat + " , " + strRep + " , " + (bGlobal ? "g" : "") + (bStop ? "s" : "");
755 settings.insert(settings.begin() + i++, 1, strReplacer);
757 settings.push_back(strReplacer);
761 // error message about missing tag
763 CLog::Log(LOGERROR," Missing <Pat> tag");
765 CLog::Log(LOGERROR," Missing <Rep> tag");
769 pReplacer = pReplacer->NextSiblingElement("replacer");