1 /* 2 * Copyright (C) 2010 Igalia S.L 3 * 4 * This library is free software; you can redistribute it and/or 5 * modify it under the terms of the GNU Library General Public 6 * License as published by the Free Software Foundation; either 7 * version 2 of the License, or (at your option) any later version. 8 * 9 * This library is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 * Library General Public License for more details. 13 * 14 * You should have received a copy of the GNU Library General Public License 15 * along with this library; see the file COPYING.LIB. If not, write to 16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 17 * Boston, MA 02110-1301, USA. 18 */ 19 20 #include "config.h" 21 22 #if ENABLE(VIDEO) 23 24 #include "FullscreenVideoController.h" 25 26 #include "GRefPtrGtk.h" 27 #include "GtkVersioning.h" 28 #include "MediaPlayer.h" 29 30 #include <gdk/gdk.h> 31 #include <gdk/gdkkeysyms.h> 32 #include <glib/gi18n-lib.h> 33 #include <gst/gst.h> 34 #include <gtk/gtk.h> 35 36 using namespace std; 37 using namespace WebCore; 38 39 #define HUD_AUTO_HIDE_INTERVAL 3000 // 3 seconds 40 #define PROGRESS_BAR_UPDATE_INTERVAL 150 // 150ms 41 #define VOLUME_UP_OFFSET 0.05 // 5% 42 #define VOLUME_DOWN_OFFSET 0.05 // 5% 43 44 // Use symbolic icons only if we build with GTK+-3 support. They could 45 // be enabled for the GTK+2 build but we'd need to bump the required 46 // version to at least 2.22. 47 #if GTK_MAJOR_VERSION < 3 48 #define PLAY_ICON_NAME "media-playback-start" 49 #define PAUSE_ICON_NAME "media-playback-pause" 50 #define EXIT_FULLSCREEN_ICON_NAME "view-restore" 51 #else 52 #define PLAY_ICON_NAME "media-playback-start-symbolic" 53 #define PAUSE_ICON_NAME "media-playback-pause-symbolic" 54 #define EXIT_FULLSCREEN_ICON_NAME "view-restore-symbolic" 55 #endif 56 57 static gboolean hideHudCallback(FullscreenVideoController* controller) 58 { 59 controller->hideHud(); 60 return FALSE; 61 } 62 63 static gboolean onFullscreenGtkMotionNotifyEvent(GtkWidget* widget, GdkEventMotion* event, FullscreenVideoController* controller) 64 { 65 controller->showHud(true); 66 return TRUE; 67 } 68 69 static void onFullscreenGtkActiveNotification(GtkWidget* widget, GParamSpec* property, FullscreenVideoController* controller) 70 { 71 if (!gtk_window_is_active(GTK_WINDOW(widget))) 72 controller->hideHud(); 73 } 74 75 static gboolean onFullscreenGtkConfigureEvent(GtkWidget* widget, GdkEventConfigure* event, FullscreenVideoController* controller) 76 { 77 controller->gtkConfigure(event); 78 return TRUE; 79 } 80 81 static void onFullscreenGtkDestroy(GtkWidget* widget, FullscreenVideoController* controller) 82 { 83 controller->exitFullscreen(); 84 } 85 86 static void togglePlayPauseActivated(GtkAction* action, FullscreenVideoController* controller) 87 { 88 controller->togglePlay(); 89 } 90 91 static void exitFullscreenActivated(GtkAction* action, FullscreenVideoController* controller) 92 { 93 controller->exitOnUserRequest(); 94 } 95 96 static gboolean progressBarUpdateCallback(FullscreenVideoController* controller) 97 { 98 return controller->updateHudProgressBar(); 99 } 100 101 static gboolean timeScaleButtonPressed(GtkWidget* widget, GdkEventButton* event, FullscreenVideoController* controller) 102 { 103 if (event->type != GDK_BUTTON_PRESS) 104 return FALSE; 105 106 controller->beginSeek(); 107 return FALSE; 108 } 109 110 static gboolean timeScaleButtonReleased(GtkWidget* widget, GdkEventButton* event, FullscreenVideoController* controller) 111 { 112 controller->endSeek(); 113 return FALSE; 114 } 115 116 static void timeScaleValueChanged(GtkWidget* widget, FullscreenVideoController* controller) 117 { 118 controller->doSeek(); 119 } 120 121 static void volumeValueChanged(GtkScaleButton *button, gdouble value, FullscreenVideoController* controller) 122 { 123 controller->setVolume(static_cast<float>(value)); 124 } 125 126 void playerVolumeChangedCallback(GObject *element, GParamSpec *pspec, FullscreenVideoController* controller) 127 { 128 controller->volumeChanged(); 129 } 130 131 void playerMuteChangedCallback(GObject *element, GParamSpec *pspec, FullscreenVideoController* controller) 132 { 133 controller->muteChanged(); 134 } 135 136 FullscreenVideoController::FullscreenVideoController() 137 : m_hudTimeoutId(0) 138 , m_progressBarUpdateId(0) 139 , m_seekLock(false) 140 , m_window(0) 141 , m_hudWindow(0) 142 { 143 } 144 145 FullscreenVideoController::~FullscreenVideoController() 146 { 147 exitFullscreen(); 148 } 149 150 void FullscreenVideoController::setMediaElement(HTMLMediaElement* mediaElement) 151 { 152 if (mediaElement == m_mediaElement) 153 return; 154 155 m_mediaElement = mediaElement; 156 if (!m_mediaElement) { 157 // Can't do full-screen, just get out 158 exitFullscreen(); 159 } 160 } 161 162 void FullscreenVideoController::gtkConfigure(GdkEventConfigure* event) 163 { 164 updateHudPosition(); 165 } 166 167 void FullscreenVideoController::showHud(bool autoHide) 168 { 169 if (!m_hudWindow) 170 return; 171 172 if (m_hudTimeoutId) { 173 g_source_remove(m_hudTimeoutId); 174 m_hudTimeoutId = 0; 175 } 176 177 // Show the cursor. 178 GdkWindow* window = gtk_widget_get_window(m_window); 179 gdk_window_set_cursor(window, 0); 180 181 // Update the progress bar immediately before showing the window. 182 updateHudProgressBar(); 183 gtk_widget_show_all(m_hudWindow); 184 updateHudPosition(); 185 186 // Start periodic updates of the progress bar. 187 if (!m_progressBarUpdateId) 188 m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast<GSourceFunc>(progressBarUpdateCallback), this); 189 190 // Hide the hud in few seconds, if requested. 191 if (autoHide) 192 m_hudTimeoutId = g_timeout_add(HUD_AUTO_HIDE_INTERVAL, reinterpret_cast<GSourceFunc>(hideHudCallback), this); 193 } 194 195 void FullscreenVideoController::hideHud() 196 { 197 if (m_hudTimeoutId) { 198 g_source_remove(m_hudTimeoutId); 199 m_hudTimeoutId = 0; 200 } 201 202 if (!m_hudWindow) 203 return; 204 205 // Keep the hud visible if a seek is in progress or if the volume 206 // popup is visible. 207 GtkWidget* volumePopup = gtk_scale_button_get_popup(GTK_SCALE_BUTTON(m_volumeButton)); 208 if (m_seekLock || gtk_widget_get_visible(volumePopup)) { 209 showHud(true); 210 return; 211 } 212 213 GdkWindow* window = gtk_widget_get_window(m_window); 214 GdkCursor* cursor = blankCursor(); 215 gdk_window_set_cursor(window, cursor); 216 217 gtk_widget_hide(m_hudWindow); 218 219 if (m_progressBarUpdateId) { 220 g_source_remove(m_progressBarUpdateId); 221 m_progressBarUpdateId = 0; 222 } 223 } 224 225 static gboolean onFullscreenGtkKeyPressEvent(GtkWidget* widget, GdkEventKey* event, FullscreenVideoController* controller) 226 { 227 switch (event->keyval) { 228 case GDK_Escape: 229 case 'f': 230 case 'F': 231 controller->exitOnUserRequest(); 232 break; 233 case GDK_space: 234 case GDK_Return: 235 controller->togglePlay(); 236 break; 237 case GDK_Up: 238 // volume up 239 controller->setVolume(controller->volume() + VOLUME_UP_OFFSET); 240 break; 241 case GDK_Down: 242 // volume down 243 controller->setVolume(controller->volume() - VOLUME_DOWN_OFFSET); 244 break; 245 default: 246 break; 247 } 248 249 return TRUE; 250 } 251 252 253 void FullscreenVideoController::enterFullscreen() 254 { 255 if (!m_mediaElement) 256 return; 257 258 if (m_mediaElement->platformMedia().type != WebCore::PlatformMedia::GStreamerGWorldType) 259 return; 260 261 m_gstreamerGWorld = m_mediaElement->platformMedia().media.gstreamerGWorld; 262 if (!m_gstreamerGWorld->enterFullscreen()) 263 return; 264 265 m_window = reinterpret_cast<GtkWidget*>(m_gstreamerGWorld->platformVideoWindow()->window()); 266 267 GstElement* pipeline = m_gstreamerGWorld->pipeline(); 268 g_signal_connect(pipeline, "notify::volume", G_CALLBACK(playerVolumeChangedCallback), this); 269 g_signal_connect(pipeline, "notify::mute", G_CALLBACK(playerMuteChangedCallback), this); 270 271 if (!m_hudWindow) 272 createHud(); 273 274 // Ensure black background. 275 #ifdef GTK_API_VERSION_2 276 GdkColor color = { 1, 0, 0, 0 }; 277 gtk_widget_modify_bg(m_window, GTK_STATE_NORMAL, &color); 278 #else 279 GdkRGBA color = { 0, 0, 0, 1}; 280 gtk_widget_override_background_color(m_window, GTK_STATE_FLAG_NORMAL, &color); 281 #endif 282 gtk_widget_set_double_buffered(m_window, FALSE); 283 284 g_signal_connect(m_window, "key-press-event", G_CALLBACK(onFullscreenGtkKeyPressEvent), this); 285 g_signal_connect(m_window, "destroy", G_CALLBACK(onFullscreenGtkDestroy), this); 286 g_signal_connect(m_window, "notify::is-active", G_CALLBACK(onFullscreenGtkActiveNotification), this); 287 288 gtk_widget_show_all(m_window); 289 290 GdkWindow* window = gtk_widget_get_window(m_window); 291 GRefPtr<GdkCursor> cursor(adoptGRef(blankCursor())); 292 gdk_window_set_cursor(window, cursor.get()); 293 294 g_signal_connect(m_window, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this); 295 g_signal_connect(m_window, "configure-event", G_CALLBACK(onFullscreenGtkConfigureEvent), this); 296 297 gtk_window_fullscreen(GTK_WINDOW(m_window)); 298 showHud(true); 299 } 300 301 void FullscreenVideoController::updateHudPosition() 302 { 303 if (!m_hudWindow) 304 return; 305 306 // Get the screen rectangle. 307 GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_window)); 308 GdkWindow* window = gtk_widget_get_window(m_window); 309 GdkRectangle fullscreenRectangle; 310 gdk_screen_get_monitor_geometry(screen, gdk_screen_get_monitor_at_window(screen, window), 311 &fullscreenRectangle); 312 313 // Get the popup window size. 314 int hudWidth, hudHeight; 315 gtk_window_get_size(GTK_WINDOW(m_hudWindow), &hudWidth, &hudHeight); 316 317 // Resize the hud to the full width of the screen. 318 gtk_window_resize(GTK_WINDOW(m_hudWindow), fullscreenRectangle.width, hudHeight); 319 320 // Move the hud to the bottom of the screen. 321 gtk_window_move(GTK_WINDOW(m_hudWindow), fullscreenRectangle.x, 322 fullscreenRectangle.height + fullscreenRectangle.y - hudHeight); 323 } 324 325 void FullscreenVideoController::exitOnUserRequest() 326 { 327 m_mediaElement->exitFullscreen(); 328 } 329 330 void FullscreenVideoController::exitFullscreen() 331 { 332 if (!m_hudWindow) 333 return; 334 335 g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast<void*>(onFullscreenGtkKeyPressEvent), this); 336 g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast<void*>(onFullscreenGtkDestroy), this); 337 g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast<void*>(onFullscreenGtkMotionNotifyEvent), this); 338 g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast<void*>(onFullscreenGtkConfigureEvent), this); 339 340 GstElement* pipeline = m_mediaElement->platformMedia().media.gstreamerGWorld->pipeline(); 341 g_signal_handlers_disconnect_by_func(pipeline, reinterpret_cast<void*>(playerVolumeChangedCallback), this); 342 g_signal_handlers_disconnect_by_func(pipeline, reinterpret_cast<void*>(playerMuteChangedCallback), this); 343 344 if (m_hudTimeoutId) { 345 g_source_remove(m_hudTimeoutId); 346 m_hudTimeoutId = 0; 347 } 348 349 if (m_progressBarUpdateId) { 350 g_source_remove(m_progressBarUpdateId); 351 m_progressBarUpdateId = 0; 352 } 353 354 if (m_mediaElement->platformMedia().type == WebCore::PlatformMedia::GStreamerGWorldType) 355 m_mediaElement->platformMedia().media.gstreamerGWorld->exitFullscreen(); 356 357 gtk_widget_hide(m_window); 358 359 gtk_widget_destroy(m_hudWindow); 360 m_hudWindow = 0; 361 } 362 363 bool FullscreenVideoController::canPlay() const 364 { 365 return m_mediaElement && m_mediaElement->canPlay(); 366 } 367 368 void FullscreenVideoController::play() 369 { 370 if (m_mediaElement) 371 m_mediaElement->play(m_mediaElement->processingUserGesture()); 372 373 playStateChanged(); 374 showHud(true); 375 } 376 377 void FullscreenVideoController::pause() 378 { 379 if (m_mediaElement) 380 m_mediaElement->pause(m_mediaElement->processingUserGesture()); 381 382 playStateChanged(); 383 showHud(false); 384 } 385 386 void FullscreenVideoController::playStateChanged() 387 { 388 if (canPlay()) 389 g_object_set(m_playPauseAction, "tooltip", _("Play"), "icon-name", PLAY_ICON_NAME, NULL); 390 else 391 g_object_set(m_playPauseAction, "tooltip", _("Pause"), "icon-name", PAUSE_ICON_NAME, NULL); 392 } 393 394 void FullscreenVideoController::togglePlay() 395 { 396 if (canPlay()) 397 play(); 398 else 399 pause(); 400 } 401 402 float FullscreenVideoController::volume() const 403 { 404 return m_mediaElement ? m_mediaElement->volume() : 0; 405 } 406 407 bool FullscreenVideoController::muted() const 408 { 409 return m_mediaElement ? m_mediaElement->muted() : false; 410 } 411 412 void FullscreenVideoController::setVolume(float volume) 413 { 414 if (volume < 0.0 || volume > 1.0) 415 return; 416 417 if (m_mediaElement) { 418 ExceptionCode ec; 419 m_mediaElement->setVolume(volume, ec); 420 } 421 } 422 423 void FullscreenVideoController::volumeChanged() 424 { 425 g_signal_handler_block(m_volumeButton, m_volumeUpdateId); 426 gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), volume()); 427 g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId); 428 } 429 430 void FullscreenVideoController::muteChanged() 431 { 432 g_signal_handler_block(m_volumeButton, m_volumeUpdateId); 433 gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), muted() ? 0 : volume()); 434 g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId); 435 } 436 437 float FullscreenVideoController::currentTime() const 438 { 439 return m_mediaElement ? m_mediaElement->currentTime() : 0; 440 } 441 442 void FullscreenVideoController::setCurrentTime(float value) 443 { 444 if (m_mediaElement) { 445 ExceptionCode ec; 446 m_mediaElement->setCurrentTime(value, ec); 447 } 448 } 449 450 float FullscreenVideoController::duration() const 451 { 452 return m_mediaElement ? m_mediaElement->duration() : 0; 453 } 454 455 float FullscreenVideoController::percentLoaded() const 456 { 457 return m_mediaElement ? m_mediaElement->percentLoaded() : 0; 458 } 459 460 void FullscreenVideoController::beginSeek() 461 { 462 m_seekLock = true; 463 464 if (m_mediaElement) 465 m_mediaElement->beginScrubbing(); 466 } 467 468 void FullscreenVideoController::doSeek() 469 { 470 if (!m_seekLock) 471 return; 472 473 setCurrentTime(gtk_range_get_value(GTK_RANGE(m_timeHScale))*duration() / 100); 474 } 475 476 void FullscreenVideoController::endSeek() 477 { 478 if (m_mediaElement) 479 m_mediaElement->endScrubbing(); 480 481 m_seekLock = false; 482 } 483 484 static String timeToString(float time) 485 { 486 if (!isfinite(time)) 487 time = 0; 488 int seconds = fabsf(time); 489 int hours = seconds / (60 * 60); 490 int minutes = (seconds / 60) % 60; 491 seconds %= 60; 492 493 if (hours) { 494 if (hours > 9) 495 return String::format("%s%02d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds); 496 return String::format("%s%01d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds); 497 } 498 499 return String::format("%s%02d:%02d", (time < 0 ? "-" : ""), minutes, seconds); 500 } 501 502 gboolean FullscreenVideoController::updateHudProgressBar() 503 { 504 float mediaDuration(duration()); 505 float mediaPosition(currentTime()); 506 507 if (!m_seekLock) { 508 gdouble value = 0.0; 509 510 if (mediaPosition && mediaDuration) 511 value = (mediaPosition * 100.0) / mediaDuration; 512 513 GtkAdjustment* adjustment = gtk_range_get_adjustment(GTK_RANGE(m_timeHScale)); 514 gtk_adjustment_set_value(adjustment, value); 515 } 516 517 gtk_range_set_fill_level(GTK_RANGE(m_timeHScale), percentLoaded()* 100); 518 519 gchar* label = g_strdup_printf("%s / %s", timeToString(mediaPosition).utf8().data(), 520 timeToString(mediaDuration).utf8().data()); 521 gtk_label_set_text(GTK_LABEL(m_timeLabel), label); 522 g_free(label); 523 return TRUE; 524 } 525 526 void FullscreenVideoController::createHud() 527 { 528 m_hudWindow = gtk_window_new(GTK_WINDOW_POPUP); 529 gtk_window_set_gravity(GTK_WINDOW(m_hudWindow), GDK_GRAVITY_SOUTH_WEST); 530 gtk_window_set_type_hint(GTK_WINDOW(m_hudWindow), GDK_WINDOW_TYPE_HINT_NORMAL); 531 532 g_signal_connect(m_hudWindow, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this); 533 534 GtkWidget* hbox = gtk_hbox_new(FALSE, 4); 535 gtk_container_add(GTK_CONTAINER(m_hudWindow), hbox); 536 537 m_playPauseAction = gtk_action_new("play", _("Play / Pause"), _("Play or pause the media"), PAUSE_ICON_NAME); 538 g_signal_connect(m_playPauseAction, "activate", G_CALLBACK(togglePlayPauseActivated), this); 539 540 playStateChanged(); 541 542 GtkWidget* item = gtk_action_create_tool_item(m_playPauseAction); 543 gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0); 544 545 GtkWidget* label = gtk_label_new(_("Time:")); 546 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, TRUE, 0); 547 548 GtkAdjustment* adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 100.0, 0.1, 1.0, 1.0)); 549 m_timeHScale = gtk_hscale_new(adjustment); 550 gtk_scale_set_draw_value(GTK_SCALE(m_timeHScale), FALSE); 551 gtk_range_set_show_fill_level(GTK_RANGE(m_timeHScale), TRUE); 552 g_signal_connect(m_timeHScale, "button-press-event", G_CALLBACK(timeScaleButtonPressed), this); 553 g_signal_connect(m_timeHScale, "button-release-event", G_CALLBACK(timeScaleButtonReleased), this); 554 m_hscaleUpdateId = g_signal_connect(m_timeHScale, "value-changed", G_CALLBACK(timeScaleValueChanged), this); 555 556 gtk_box_pack_start(GTK_BOX(hbox), m_timeHScale, TRUE, TRUE, 0); 557 558 m_timeLabel = gtk_label_new(""); 559 gtk_box_pack_start(GTK_BOX(hbox), m_timeLabel, FALSE, TRUE, 0); 560 561 // Volume button. 562 m_volumeButton = gtk_volume_button_new(); 563 gtk_box_pack_start(GTK_BOX(hbox), m_volumeButton, FALSE, TRUE, 0); 564 gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), volume()); 565 m_volumeUpdateId = g_signal_connect(m_volumeButton, "value-changed", G_CALLBACK(volumeValueChanged), this); 566 567 568 m_exitFullscreenAction = gtk_action_new("exit", _("Exit Fullscreen"), _("Exit from fullscreen mode"), EXIT_FULLSCREEN_ICON_NAME); 569 g_signal_connect(m_exitFullscreenAction, "activate", G_CALLBACK(exitFullscreenActivated), this); 570 g_object_set(m_exitFullscreenAction, "icon-name", EXIT_FULLSCREEN_ICON_NAME, NULL); 571 item = gtk_action_create_tool_item(m_exitFullscreenAction); 572 gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0); 573 574 575 m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast<GSourceFunc>(progressBarUpdateCallback), this); 576 } 577 578 #endif 579