////////////////////////////////////////////////////////////////////////////// /// /// /// This file is part of the VDR mpv plugin and licensed under AGPLv3 /// /// /// /// See the README file for copyright information /// /// /// ////////////////////////////////////////////////////////////////////////////// #include #include #include #include "player.h" #include "config.h" #include "osd.h" #ifdef USE_XRANDR #include #endif using std::vector; #define MPV_OBSERVE_TIME_POS 1 #define MPV_OBSERVE_DISC_MENU 2 #define MPV_OBSERVE_FPS 3 #define MPV_OBSERVE_FILENAME 4 #define MPV_OBSERVE_LENGTH 5 #define MPV_OBSERVE_CHAPTERS 6 #define MPV_OBSERVE_CHAPTER 7 #define MPV_OBSERVE_PAUSE 8 #define MPV_OBSERVE_SPEED 9 #define MPV_OBSERVE_MEDIA_TITLE 10 volatile int cMpvPlayer::running = 0; cMpvPlayer *cMpvPlayer::PlayerHandle = NULL; // check mpv errors and send them to log static inline void check_error(int status) { if (status < 0) { esyslog("[mpv] API error: %s\n", mpv_error_string(status)); } } void *cMpvPlayer::ObserverThread(void *handle) { cMpvPlayer *Player = (cMpvPlayer*) handle; struct mpv_event_log_message *msg; // set properties which should be observed mpv_observe_property(Player->hMpv, MPV_OBSERVE_TIME_POS, "time-pos", MPV_FORMAT_DOUBLE); mpv_observe_property(Player->hMpv, MPV_OBSERVE_DISC_MENU, "disc-menu-active", MPV_FORMAT_FLAG); mpv_observe_property(Player->hMpv, MPV_OBSERVE_FPS, "fps", MPV_FORMAT_DOUBLE); mpv_observe_property(Player->hMpv, MPV_OBSERVE_FILENAME, "filename", MPV_FORMAT_STRING); mpv_observe_property(Player->hMpv, MPV_OBSERVE_LENGTH, "length", MPV_FORMAT_DOUBLE); mpv_observe_property(Player->hMpv, MPV_OBSERVE_CHAPTERS, "chapters", MPV_FORMAT_INT64); mpv_observe_property(Player->hMpv, MPV_OBSERVE_CHAPTER, "chapter", MPV_FORMAT_INT64); mpv_observe_property(Player->hMpv, MPV_OBSERVE_PAUSE, "pause", MPV_FORMAT_FLAG); mpv_observe_property(Player->hMpv, MPV_OBSERVE_SPEED, "speed", MPV_FORMAT_DOUBLE); mpv_observe_property(Player->hMpv, MPV_OBSERVE_MEDIA_TITLE, "media-title", MPV_FORMAT_STRING); while (Player->PlayerIsRunning()) { mpv_event *event = mpv_wait_event(Player->hMpv, 10000); switch (event->event_id) { case MPV_EVENT_SHUTDOWN : Player->running = 0; break; case MPV_EVENT_PROPERTY_CHANGE : Player->HandlePropertyChange(event); break; case MPV_EVENT_PLAYBACK_RESTART : Player->ChangeFrameRate(Player->CurrentFps()); // switching directly after the fps event causes black screen break; case MPV_EVENT_LOG_MESSAGE : msg = (struct mpv_event_log_message *)event->data; // without DEBUG log to error since we only request error messages from mpv in this case #ifdef DEBUG dsyslog("[mpv]: %s\n", msg->text); #else esyslog("[mpv]: %s\n", msg->text); #endif break; case MPV_EVENT_TRACKS_CHANGED : Player->HandleTracksChange(); break; case MPV_EVENT_NONE : case MPV_EVENT_END_FILE : case MPV_EVENT_PAUSE : case MPV_EVENT_UNPAUSE : case MPV_EVENT_FILE_LOADED : case MPV_EVENT_VIDEO_RECONFIG : case MPV_EVENT_GET_PROPERTY_REPLY : case MPV_EVENT_SET_PROPERTY_REPLY : case MPV_EVENT_COMMAND_REPLY : case MPV_EVENT_START_FILE : case MPV_EVENT_TRACK_SWITCHED : case MPV_EVENT_IDLE : case MPV_EVENT_TICK : case MPV_EVENT_SCRIPT_INPUT_DISPATCH : case MPV_EVENT_CLIENT_MESSAGE : case MPV_EVENT_AUDIO_RECONFIG : case MPV_EVENT_METADATA_UPDATE : case MPV_EVENT_SEEK : case MPV_EVENT_CHAPTER_CHANGE : default : dsyslog("[mpv]: event: %d %s\n", event->event_id, mpv_event_name(event->event_id)); break; } } dsyslog("[mpv] Observer thread ended\n"); return handle; } cMpvPlayer::cMpvPlayer(string Filename, bool Shuffle) :cPlayer(pmExtern_THIS_SHOULD_BE_AVOIDED) { PlayerHandle = this; PlayFilename = Filename; PlayShuffle = Shuffle; running = 0; OriginalFps = -1; } cMpvPlayer::~cMpvPlayer() { dsyslog("[mpv]%s: end\n", __FUNCTION__); Detach(); PlayerHandle = NULL; } void cMpvPlayer::Activate(bool on) { if (on) PlayerStart(); } void cMpvPlayer::SetAudioTrack(eTrackType Type, const tTrackId *TrackId) { SetAudio(TrackId->id); } void cMpvPlayer::SetSubtitleTrack(eTrackType Type, const tTrackId *TrackId) { if (Type == ttNone) { check_error(mpv_set_option_string(hMpv, "sub-forced-only", "yes")); return; } check_error(mpv_set_option_string(hMpv, "sub-forced-only", "no")); SetSubtitle(TrackId->id); } bool cMpvPlayer::GetReplayMode(bool &Play, bool &Forward, int &Speed) { Speed = CurrentPlaybackSpeed(); if (Speed == 1) Speed = -1; Forward = true; Play = !IsPaused(); return true; } void cMpvPlayer::PlayerStart() { PlayerPaused = 0; PlayerSpeed = 1; PlayerDiscNav = 0; SwitchOsdToMpv(); //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // we are cheating here with mpv since it checks for LC_NUMERIC=C at startup // this can cause unforseen issues with mpv //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! std::string LocaleSave = setlocale(LC_NUMERIC, NULL); dsyslog ("get locale %s\n", LocaleSave.c_str()); setlocale(LC_NUMERIC, "C"); hMpv = mpv_create(); if (!hMpv) { esyslog("[mpv] failed to create context\n"); cControl::Shutdown(); } int64_t osdlevel = 0; check_error(mpv_set_option_string(hMpv, "vo", MpvPluginConfig->VideoOut.c_str())); check_error(mpv_set_option_string(hMpv, "hwdec", MpvPluginConfig->HwDec.c_str())); check_error(mpv_set_option_string(hMpv, "ao", MpvPluginConfig->AudioOut.c_str())); check_error(mpv_set_option_string(hMpv, "hwdec-codecs", MpvPluginConfig->HwDec.c_str())); check_error(mpv_set_option_string(hMpv, "slang", MpvPluginConfig->Languages.c_str())); check_error(mpv_set_option_string(hMpv, "alang", MpvPluginConfig->Languages.c_str())); check_error(mpv_set_option_string(hMpv, "cache", "no")); // video stutters if enabled check_error(mpv_set_option_string(hMpv, "fullscreen", "yes")); check_error(mpv_set_option_string(hMpv, "sub-forced-only", "yes")); check_error(mpv_set_option_string(hMpv, "ontop", "yes")); check_error(mpv_set_option_string(hMpv, "cursor-autohide", "always")); check_error(mpv_set_option_string(hMpv, "stop-playback-on-init-failure", "no")); check_error(mpv_set_option_string(hMpv, "idle", "once")); check_error(mpv_set_option(hMpv, "osd-level", MPV_FORMAT_INT64, &osdlevel)); if (MpvPluginConfig->UsePassthrough) { check_error(mpv_set_option_string(hMpv, "ad", "-spdif:mp3,-spdif:aac,spdif:*")); if (MpvPluginConfig->UseDtsHdPassthrough) check_error(mpv_set_option_string(hMpv, "ad-spdif-dtshd", "yes")); } else { int64_t StartVolume = cDevice::CurrentVolume() / 2.55; check_error(mpv_set_option(hMpv, "volume", MPV_FORMAT_INT64, &StartVolume)); if (MpvPluginConfig->StereoDownmix) { check_error(mpv_set_option_string(hMpv, "ad-lavc-downmix", "yes")); check_error(mpv_set_option_string(hMpv, "audio-channels", "2")); } } if (PlayShuffle && IsPlaylist(PlayFilename)) check_error(mpv_set_option_string(hMpv, "shuffle", "yes")); if (MpvPluginConfig->NoScripts) check_error(mpv_set_option_string(hMpv, "load-scripts", "no")); #ifdef DEBUG mpv_request_log_messages(hMpv, "info"); #else mpv_request_log_messages(hMpv, "error"); #endif if (mpv_initialize(hMpv) < 0) { esyslog("[mpv] failed to initialize\n"); return; } running = 1; isyslog("[mpv] playing %s\n", PlayFilename.c_str()); if (!IsPlaylist(PlayFilename)) { const char *cmd[] = {"loadfile", PlayFilename.c_str(), NULL}; mpv_command(hMpv, cmd); } else { const char *cmd[] = {"loadlist", PlayFilename.c_str(), NULL}; mpv_command(hMpv, cmd); } // start thread to observe and react on mpv events pthread_create(&ObserverThreadHandle, NULL, ObserverThread, this); // set back locale setlocale(LC_NUMERIC, LocaleSave.c_str()); } void cMpvPlayer::HandlePropertyChange(mpv_event *event) { mpv_event_property *property = (mpv_event_property *) event->data; if (!property->data) return; // don't log on time-pos change since this floods the log if (event->reply_userdata != MPV_OBSERVE_TIME_POS) { dsyslog("[mpv]: property %s \n", property->name); } switch (event->reply_userdata) { case MPV_OBSERVE_TIME_POS : PlayerCurrent = (int)*(double*)property->data; break; case MPV_OBSERVE_DISC_MENU : PlayerDiscNav = (int)*(int64_t*)property->data; break; case MPV_OBSERVE_FPS : PlayerFps = (int)*(double*)property->data; break; case MPV_OBSERVE_FILENAME : PlayerFilename = *(char**)property->data; break; case MPV_OBSERVE_LENGTH : PlayerTotal = (int)*(double*)property->data; break; case MPV_OBSERVE_CHAPTERS : PlayerNumChapters = (int)*(int64_t*)property->data; mpv_node Node; mpv_get_property(hMpv, "chapter-list", MPV_FORMAT_NODE, &Node); ChapterTitles.clear(); PlayerChapters.clear(); if (Node.format == MPV_FORMAT_NODE_ARRAY) { for (int i=0; inum; i++) { ChapterTitles.push_back (Node.u.list->values[i].u.list->values[0].u.string); PlayerChapters.push_back (Node.u.list->values[i].u.list->values[1].u.double_); } } break; case MPV_OBSERVE_CHAPTER : PlayerChapter = (int)*(int64_t*)property->data; break; case MPV_OBSERVE_PAUSE : PlayerPaused = (int)*(int64_t*)property->data; break; case MPV_OBSERVE_SPEED : PlayerSpeed = (int)*(double*)property->data; break; case MPV_OBSERVE_MEDIA_TITLE : mediaTitle = *(char**)property->data; break; } } void cMpvPlayer::HandleTracksChange() { mpv_node Node; mpv_get_property(hMpv, "track-list", MPV_FORMAT_NODE, &Node); if (!Node.format == MPV_FORMAT_NODE_ARRAY) return; // loop though available tracks for (int i=0; inum; i++) { int TrackId; string TrackType; string TrackLanguage = "undefined"; string TrackTitle = ""; for (int j=0; jvalues[i].u.list->num; j++) { if (strcmp(Node.u.list->values[i].u.list->keys[j], "id") == 0) TrackId = Node.u.list->values[i].u.list->values[j].u.int64; if (strcmp(Node.u.list->values[i].u.list->keys[j], "type") == 0) TrackType = Node.u.list->values[i].u.list->values[j].u.string; if (strcmp(Node.u.list->values[i].u.list->keys[j], "lang") == 0) TrackLanguage = Node.u.list->values[i].u.list->values[j].u.string; if (strcmp(Node.u.list->values[i].u.list->keys[j], "title") == 0) TrackTitle = Node.u.list->values[i].u.list->values[j].u.string; } if (TrackType == "audio") { eTrackType type = ttAudio; DeviceSetAvailableTrack(type, i, TrackId, TrackLanguage.c_str(), TrackTitle.c_str()); } else if (TrackType == "sub") { eTrackType type = ttSubtitle; DeviceSetAvailableTrack(type, 0, 0, "Off"); DeviceSetAvailableTrack(type, i, TrackId, TrackLanguage.c_str(), TrackTitle.c_str()); } } } void cMpvPlayer::OsdClose() { dsyslog("[mpv] %s\n", __FUNCTION__); SendCommand ("overlay_remove 1"); } void cMpvPlayer::Shutdown() { mediaTitle = ""; running = 0; MpvPluginConfig->TitleOverride = ""; ChapterTitles.clear(); PlayerChapters.clear(); if (ObserverThreadHandle) pthread_cancel(ObserverThreadHandle); mpv_terminate_destroy(hMpv); hMpv = NULL; cOsdProvider::Shutdown(); if (MpvPluginConfig->RefreshRate) { ChangeFrameRate(OriginalFps); OriginalFps = -1; } } void cMpvPlayer::SwitchOsdToMpv() { dsyslog("[mpv] %s\n", __FUNCTION__); cOsdProvider::Shutdown(); new cMpvOsdProvider(this); } bool cMpvPlayer::IsPlaylist(string File) { for (unsigned int i=0;iPlaylistExtensions.size();i++) { if (File.substr(File.find_last_of(".") + 1) == MpvPluginConfig->PlaylistExtensions[i]) return true; } return false; } void cMpvPlayer::ChangeFrameRate(int TargetRate) { #ifdef USE_XRANDR if (!MpvPluginConfig->RefreshRate) return; Display *Dpy; int RefreshRate; XRRScreenConfiguration *CurrInfo; if (TargetRate == 25) TargetRate = 50; // fix DVD audio and since this is doubled it's ok if (TargetRate == 23) TargetRate = 24; Dpy = XOpenDisplay(MpvPluginConfig->X11Display.c_str()); if (Dpy) { short *Rates; int NumberOfRates; SizeID CurrentSizeId; Rotation CurrentRotation; int RateFound = 0; CurrInfo = XRRGetScreenInfo(Dpy, DefaultRootWindow(Dpy)); RefreshRate = XRRConfigCurrentRate(CurrInfo); CurrentSizeId = XRRConfigCurrentConfiguration(CurrInfo, &CurrentRotation); Rates = XRRConfigRates(CurrInfo, CurrentSizeId, &NumberOfRates); while (NumberOfRates-- > 0) { if (TargetRate == *Rates++) RateFound = 1; } if ((RefreshRate != TargetRate) && (RateFound == 1)) { OriginalFps = RefreshRate; XRRSetScreenConfigAndRate(Dpy, CurrInfo, DefaultRootWindow(Dpy), CurrentSizeId, CurrentRotation, TargetRate, CurrentTime); } XRRFreeScreenConfigInfo(CurrInfo); XCloseDisplay(Dpy); } #endif } void cMpvPlayer::SendCommand(const char *cmd, ...) { if (!PlayerIsRunning()) return; va_list va; char buf[256]; va_start(va, cmd); vsnprintf(buf, sizeof(buf), cmd, va); va_end(va); mpv_command_string(hMpv, buf); } void cMpvPlayer::Seek(int Seconds) { SendCommand("seek %d\n", Seconds); } void cMpvPlayer::SetTimePos(int Seconds) { SendCommand("set time-pos %d\n", Seconds); } void cMpvPlayer::SetSpeed(int Speed) { SendCommand("set speed %d\n", Speed); } void cMpvPlayer::SetAudio(int Audio) { SendCommand("set audio %d\n", Audio); } void cMpvPlayer::SetSubtitle(int Subtitle) { SendCommand("set sub %d\n", Subtitle); } void cMpvPlayer::SetChapter(int Chapter) { SendCommand("set chapter %d\n", Chapter-1); } void cMpvPlayer::TogglePause() { SendCommand("cycle pause\n"); } void cMpvPlayer::QuitPlayer() { SendCommand("quit\n"); } void cMpvPlayer::DiscNavUp() { SendCommand("discnav up\n"); } void cMpvPlayer::DiscNavDown() { SendCommand("discnav down\n"); } void cMpvPlayer::DiscNavLeft() { SendCommand("discnav left\n"); } void cMpvPlayer::DiscNavRight() { SendCommand("discnav right\n"); } void cMpvPlayer::DiscNavMenu() { SendCommand("discnav menu\n"); } void cMpvPlayer::DiscNavSelect() { SendCommand("discnav select\n"); } void cMpvPlayer::DiscNavPrev() { SendCommand("discnav prev\n"); } void cMpvPlayer::PreviousChapter() { SendCommand("cycle chapter -1\n"); } void cMpvPlayer::NextChapter() { SendCommand("cycle chapter 1\n"); } void cMpvPlayer::NextPlaylistItem() { SendCommand("pt_step 1\n"); } void cMpvPlayer::PreviousPlaylistItem() { SendCommand("pt_step -1\n"); } void cMpvPlayer::SetVolume(int Volume) { SendCommand("set volume %d\n", Volume); }