Home | History | Annotate | Download | only in WebCoreSupport
      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