GLPlayer.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. #include "GLPlayer.h"
  2. #include "WallpaperEngine/Logging/Log.h"
  3. #include <mpv/render_gl.h>
  4. #include <mpv/stream_cb.h>
  5. #include <chrono>
  6. #include <cstdlib>
  7. #include <string_view>
  8. #include <vector>
  9. using namespace WallpaperEngine::VideoPlayback::MPV;
  10. std::unordered_map<std::filesystem::path, GLPlayer*> GLPlayer::s_activePlayers;
  11. double GLPlayer::s_statsMillis = 0.0;
  12. // the synchronous mpv_set_property/mpv_command wait for mpv's core thread, which can take a few hundred ms
  13. // while it's busy, and these are called from the render thread (scripts pausing/seeking videos on click)
  14. static void setPropertyAsync (mpv_handle* handle, const char* name, const char* value) {
  15. mpv_set_property_async (handle, 0, name, MPV_FORMAT_STRING, &value);
  16. }
  17. void* get_proc_address (void* ctx, const char* name) {
  18. return static_cast<GLPlayer*> (ctx)->getContext ().getDriver ().getProcAddress (name);
  19. }
  20. GLPlayer::GLPlayer (
  21. RenderContext& context, GLuint outputTexture, const std::filesystem::path& file, const int64_t baseWidth,
  22. const int64_t baseHeight, const GLuint fbo
  23. ) : ContextAware (context), m_outputTexture (outputTexture), m_width (baseWidth), m_height (baseHeight) {
  24. this->m_fbo = fbo;
  25. this->m_doWeOwnFramebuffer = this->m_fbo == GL_NONE;
  26. this->prepareGL ();
  27. this->setSource (file);
  28. }
  29. GLPlayer::GLPlayer (
  30. RenderContext& context, const GLuint outputTexture, MemoryStreamProtocolUniquePtr stream, const int64_t baseWidth,
  31. const int64_t baseHeight, const GLuint fbo
  32. ) : ContextAware (context), m_outputTexture (outputTexture), m_width (baseWidth), m_height (baseHeight) {
  33. this->m_fbo = fbo;
  34. this->m_doWeOwnFramebuffer = this->m_fbo == GL_NONE;
  35. this->prepareGL ();
  36. this->setSource (std::move (stream));
  37. }
  38. GLPlayer::~GLPlayer () {
  39. this->stop ();
  40. if (this->m_doWeOwnFramebuffer) {
  41. glDeleteFramebuffers (1, &this->m_fbo);
  42. }
  43. }
  44. void GLPlayer::incrementUsageCount () {
  45. this->m_usageCount++;
  46. if (this->m_usageCount == 1) {
  47. this->play ();
  48. }
  49. }
  50. void GLPlayer::decrementUsageCount () {
  51. if (this->m_usageCount == 0) {
  52. sLog.exception ("GLPlayer usage count would underflow");
  53. }
  54. this->m_usageCount--;
  55. if (this->m_usageCount == 0) {
  56. this->stop ();
  57. }
  58. }
  59. void GLPlayer::setUntimed () {
  60. if (this->m_handle) {
  61. sLog.exception ("Cannot set untimed mode after playback has started");
  62. }
  63. this->m_untimed = true;
  64. }
  65. void GLPlayer::disableAudio () {
  66. if (this->m_handle) {
  67. sLog.exception ("Cannot disable audio after playback has started");
  68. }
  69. this->m_audio = false;
  70. }
  71. void GLPlayer::clearUntimed () {
  72. if (this->m_handle) {
  73. sLog.exception ("Cannot set untimed mode after playback has started");
  74. }
  75. this->m_untimed = false;
  76. }
  77. void GLPlayer::setMuted () {
  78. this->m_muted = true;
  79. if (this->m_handle) {
  80. setPropertyAsync (this->m_handle, "mute", "yes");
  81. }
  82. }
  83. void GLPlayer::clearMuted () {
  84. this->m_muted = false;
  85. if (this->m_handle) {
  86. setPropertyAsync (this->m_handle, "mute", "no");
  87. }
  88. }
  89. void GLPlayer::setVolume (double volume) {
  90. this->m_volume = volume;
  91. if (this->m_handle) {
  92. mpv_set_property_async (this->m_handle, 0, "volume", MPV_FORMAT_DOUBLE, &this->m_volume);
  93. }
  94. }
  95. void GLPlayer::setSpeed (double speed) {
  96. this->m_speed = speed;
  97. if (this->m_handle) {
  98. mpv_set_property_async (this->m_handle, 0, "speed", MPV_FORMAT_DOUBLE, &this->m_speed);
  99. }
  100. }
  101. void GLPlayer::setPaused () {
  102. this->m_paused = true;
  103. if (this->m_handle) {
  104. setPropertyAsync (this->m_handle, "pause", "yes");
  105. }
  106. }
  107. void GLPlayer::clearPaused () {
  108. this->m_paused = false;
  109. if (this->m_handle) {
  110. setPropertyAsync (this->m_handle, "pause", "no");
  111. }
  112. }
  113. void GLPlayer::setLoop (bool loop) {
  114. this->m_loop = loop;
  115. if (this->m_handle) {
  116. setPropertyAsync (this->m_handle, "loop", loop ? "inf" : "no");
  117. }
  118. }
  119. void GLPlayer::seek (const double seconds) {
  120. this->m_ended = false;
  121. if (this->m_handle == nullptr || !this->m_fileLoaded) {
  122. this->m_pendingSeek = seconds;
  123. return;
  124. }
  125. const std::string target = std::to_string (seconds);
  126. const char* command[] = { "seek", target.c_str (), "absolute+exact", nullptr };
  127. mpv_command_async (this->m_handle, 0, command);
  128. }
  129. double GLPlayer::getDuration () const {
  130. double duration = 0.0;
  131. if (this->m_handle != nullptr) {
  132. mpv_get_property (this->m_handle, "duration", MPV_FORMAT_DOUBLE, &duration);
  133. }
  134. return duration;
  135. }
  136. void GLPlayer::render () const {
  137. // only render while actively playing (m_handle is set by usage-count-driven play())
  138. if (this->m_handle == nullptr) {
  139. return;
  140. }
  141. while (true) {
  142. const mpv_event* event = mpv_wait_event (this->m_handle, 0);
  143. if (event == nullptr || event->event_id == MPV_EVENT_NONE) {
  144. break;
  145. }
  146. if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) {
  147. const auto* property = static_cast<const mpv_event_property*> (event->data);
  148. if (property->format == MPV_FORMAT_FLAG && std::string_view (property->name) == "eof-reached"
  149. && *static_cast<const int*> (property->data) && !this->m_loop) {
  150. this->m_ended = true;
  151. }
  152. continue;
  153. }
  154. if (event->event_id == MPV_EVENT_FILE_LOADED) {
  155. this->m_fileLoaded = true;
  156. if (this->m_pendingSeek.has_value ()) {
  157. const std::string target = std::to_string (*this->m_pendingSeek);
  158. const char* command[] = { "seek", target.c_str (), "absolute+exact", nullptr };
  159. this->m_pendingSeek.reset ();
  160. mpv_command_async (this->m_handle, 0, command);
  161. }
  162. continue;
  163. }
  164. if (event->event_id != MPV_EVENT_VIDEO_RECONFIG) {
  165. continue;
  166. }
  167. int64_t width, height;
  168. if (mpv_get_property (this->m_handle, "dwidth", MPV_FORMAT_INT64, &width) < 0) {
  169. continue;
  170. }
  171. if (mpv_get_property (this->m_handle, "dheight", MPV_FORMAT_INT64, &height) < 0) {
  172. continue;
  173. }
  174. if (width < 0 || height < 0) {
  175. continue;
  176. }
  177. this->m_width = width;
  178. this->m_height = height;
  179. this->m_needsRedraw = true;
  180. glBindTexture (GL_TEXTURE_2D, this->m_outputTexture);
  181. glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, this->m_width, this->m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
  182. }
  183. // mpv only hands out its next frame once the update flags were collected, without this playback stalls
  184. const uint64_t updateFlags = mpv_render_context_update (this->m_renderContext);
  185. if (this->m_doWeOwnFramebuffer) {
  186. if (!(updateFlags & MPV_RENDER_UPDATE_FRAME) && !this->m_needsRedraw) {
  187. return;
  188. }
  189. this->m_needsRedraw = false;
  190. }
  191. glViewport (0, 0, this->m_width, this->m_height);
  192. mpv_opengl_fbo fbo { static_cast<int> (this->m_fbo), static_cast<int> (this->m_width),
  193. static_cast<int> (this->m_height), GL_RGBA8 };
  194. // no need to flip as it'll be handled by the wallpaper rendering code
  195. int flip_y = 0;
  196. // by default mpv sleeps in here until the frame's display time, stalling the whole scene
  197. // (~20ms of every 33ms frame per texture video). A texture is just sampled later, so don't wait
  198. int blockForTargetTime = this->m_doWeOwnFramebuffer ? 0 : 1;
  199. mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &fbo },
  200. { MPV_RENDER_PARAM_FLIP_Y, &flip_y },
  201. { MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &blockForTargetTime },
  202. { MPV_RENDER_PARAM_INVALID, nullptr } };
  203. static const bool collectStats = std::getenv ("LWE_FRAME_STATS") != nullptr;
  204. if (!collectStats) {
  205. mpv_render_context_render (this->m_renderContext, params);
  206. return;
  207. }
  208. glFinish ();
  209. const auto start = std::chrono::steady_clock::now ();
  210. mpv_render_context_render (this->m_renderContext, params);
  211. glFinish ();
  212. s_statsMillis += std::chrono::duration<double, std::milli> (std::chrono::steady_clock::now () - start).count ();
  213. }
  214. int GLPlayer::getWidth () const { return this->m_width; }
  215. int GLPlayer::getHeight () const { return this->m_height; }
  216. double GLPlayer::getPlaybackPosition () const {
  217. double position = 0.0;
  218. if (this->m_handle != nullptr) {
  219. mpv_get_property (this->m_handle, "time-pos", MPV_FORMAT_DOUBLE, &position);
  220. }
  221. return position;
  222. }
  223. void GLPlayer::prepareGL () {
  224. if (!this->m_doWeOwnFramebuffer || this->m_fbo != GL_NONE) {
  225. return;
  226. }
  227. glGenFramebuffers (1, &this->m_fbo);
  228. glBindFramebuffer (GL_FRAMEBUFFER, this->m_fbo);
  229. glBindTexture (GL_TEXTURE_2D, this->m_outputTexture);
  230. glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, this->m_width, this->m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
  231. constexpr GLenum drawBuffers[1] = { GL_COLOR_ATTACHMENT0 };
  232. glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this->m_outputTexture, 0);
  233. glDrawBuffers (1, drawBuffers);
  234. if (glCheckFramebufferStatus (GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
  235. sLog.exception ("Framebuffers are not properly set");
  236. }
  237. glClear (GL_COLOR_BUFFER_BIT);
  238. }
  239. void GLPlayer::init () {
  240. this->m_fileLoaded = false;
  241. this->m_ended = false;
  242. this->m_handle = mpv_create ();
  243. if (this->m_handle == nullptr) {
  244. sLog.exception ("Cannot create mpv context for video texture");
  245. }
  246. // setup mpv options for playback
  247. mpv_set_option_string (this->m_handle, "terminal", "yes");
  248. #if NDEBUG
  249. mpv_set_option_string (this->m_handle, "msg-level", "all=status,statusline=no");
  250. #else
  251. mpv_set_option_string (this->m_handle, "msg-level", "all=v");
  252. #endif
  253. mpv_set_option_string (this->m_handle, "input-cursor", "no");
  254. mpv_set_option_string (this->m_handle, "cursor-autohide", "no");
  255. mpv_set_option_string (this->m_handle, "config", "no");
  256. mpv_set_option_string (this->m_handle, "fbo-format", "rgba8");
  257. mpv_set_option_string (this->m_handle, "vo", "libmpv");
  258. mpv_set_option_string (this->m_handle, "profile", "fast");
  259. mpv_set_option_string (this->m_handle, "untimed", this->m_untimed ? "yes" : "no");
  260. if (!this->m_audio) {
  261. mpv_set_option_string (this->m_handle, "aid", "no");
  262. }
  263. if (this->m_doWeOwnFramebuffer) {
  264. // without the blocking wait in render() a frame would otherwise show up to 50ms early
  265. mpv_set_option_string (this->m_handle, "video-timing-offset", "0");
  266. }
  267. if (mpv_initialize (this->m_handle) < 0) {
  268. sLog.exception ("Could not initialize mpv context");
  269. }
  270. // keep-open parks a finished video on its last frame instead of unloading it, so a script can still seek it
  271. // back to the start. Observed rather than polled: mpv_get_property waits for mpv's core thread, and polling
  272. // it every frame stalled the render thread ~160-200ms every few seconds on a paused video
  273. mpv_observe_property (this->m_handle, 0, "eof-reached", MPV_FORMAT_FLAG);
  274. mpv_set_property_string (this->m_handle, "hwdec", "auto");
  275. mpv_set_property_string (this->m_handle, "loop", this->m_loop ? "inf" : "no");
  276. mpv_set_property_string (this->m_handle, "keep-open", "yes");
  277. mpv_set_property (this->m_handle, "volume", MPV_FORMAT_DOUBLE, &this->m_volume);
  278. mpv_set_property (this->m_handle, "speed", MPV_FORMAT_DOUBLE, &this->m_speed);
  279. // initialize gl context for mpv
  280. mpv_opengl_init_params gl_init_params { get_proc_address, this };
  281. // without the native display mpv can't import decoded frames into our GL context (VA-API dmabuf interop)
  282. // and falls back to a copy mode, every frame going GPU -> RAM -> GPU
  283. const auto& driver = this->getContext ().getDriver ();
  284. std::vector<mpv_render_param> params {
  285. { MPV_RENDER_PARAM_API_TYPE, const_cast<char*> (MPV_RENDER_API_TYPE_OPENGL) },
  286. { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
  287. };
  288. if (void* display = driver.getWaylandDisplay ()) {
  289. params.push_back ({ MPV_RENDER_PARAM_WL_DISPLAY, display });
  290. } else if (void* display = driver.getX11Display ()) {
  291. params.push_back ({ MPV_RENDER_PARAM_X11_DISPLAY, display });
  292. }
  293. params.push_back ({ MPV_RENDER_PARAM_INVALID, nullptr });
  294. if (mpv_render_context_create (&this->m_renderContext, this->m_handle, params.data ()) < 0) {
  295. sLog.exception ("Failed to initialize MPV's GL context");
  296. }
  297. mpv_set_property_string (this->m_handle, "mute", this->m_muted ? "yes" : "no");
  298. mpv_set_property_string (this->m_handle, "pause", this->m_paused ? "yes" : "no");
  299. }
  300. void GLPlayer::setSource (const std::filesystem::path& file) { this->m_file = file; }
  301. void GLPlayer::setSource (MemoryStreamProtocolUniquePtr source) { this->m_stream = std::move (source); }
  302. void GLPlayer::play () {
  303. if (this->m_handle != nullptr) {
  304. sLog.exception ("Cannot play the same GLPlayer twice");
  305. }
  306. if (!this->m_file.has_value () && !this->m_stream.has_value ()) {
  307. sLog.exception ("Cannot play a GLPlayer without a source");
  308. }
  309. this->init ();
  310. if (this->m_file.has_value ()) {
  311. const auto& path = this->m_file.value ();
  312. // if another player is already showing the same video (e.g. mirrored on another
  313. // monitor), start around the same position instead of always restarting from zero
  314. std::string startOption;
  315. std::vector<const char*> command = { "loadfile", path.c_str (), "replace" };
  316. if (const auto it = s_activePlayers.find (path); it != s_activePlayers.end ()) {
  317. if (const double position = it->second->getPlaybackPosition (); position > 0.0) {
  318. startOption = "start=" + std::to_string (position);
  319. command.push_back (startOption.c_str ());
  320. }
  321. }
  322. command.push_back (nullptr);
  323. if (mpv_command (this->m_handle, command.data ()) < 0) {
  324. sLog.exception ("Cannot load video to play");
  325. }
  326. s_activePlayers[path] = this;
  327. } else if (this->m_stream) {
  328. this->m_stream.value ()->registerReadCallback (this->m_handle);
  329. const char* command[] = { "loadfile", "buffer://", nullptr };
  330. if (mpv_command (this->m_handle, command) < 0) {
  331. sLog.exception ("Cannot load video texture to play");
  332. }
  333. }
  334. }
  335. void GLPlayer::stop () {
  336. // drop ourselves from the active players list, but only if we're still the one registered
  337. // (a newer player for the same path may have already taken over the slot)
  338. if (this->m_file.has_value ()) {
  339. if (const auto it = s_activePlayers.find (this->m_file.value ()); it != s_activePlayers.end () && it->second == this) {
  340. s_activePlayers.erase (it);
  341. }
  342. }
  343. if (this->m_renderContext) {
  344. mpv_render_context_free (this->m_renderContext);
  345. this->m_renderContext = nullptr;
  346. }
  347. if (this->m_handle) {
  348. mpv_terminate_destroy (this->m_handle);
  349. this->m_handle = nullptr;
  350. }
  351. }