/* * Copyright (C) 2010 Igalia S.L * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #if ENABLE(VIDEO) #include "FullscreenVideoController.h" #include "GRefPtrGtk.h" #include "GtkVersioning.h" #include "MediaPlayer.h" #include #include #include #include #include using namespace std; using namespace WebCore; #define HUD_AUTO_HIDE_INTERVAL 3000 // 3 seconds #define PROGRESS_BAR_UPDATE_INTERVAL 150 // 150ms #define VOLUME_UP_OFFSET 0.05 // 5% #define VOLUME_DOWN_OFFSET 0.05 // 5% // Use symbolic icons only if we build with GTK+-3 support. They could // be enabled for the GTK+2 build but we'd need to bump the required // version to at least 2.22. #if GTK_MAJOR_VERSION < 3 #define PLAY_ICON_NAME "media-playback-start" #define PAUSE_ICON_NAME "media-playback-pause" #define EXIT_FULLSCREEN_ICON_NAME "view-restore" #else #define PLAY_ICON_NAME "media-playback-start-symbolic" #define PAUSE_ICON_NAME "media-playback-pause-symbolic" #define EXIT_FULLSCREEN_ICON_NAME "view-restore-symbolic" #endif static gboolean hideHudCallback(FullscreenVideoController* controller) { controller->hideHud(); return FALSE; } static gboolean onFullscreenGtkMotionNotifyEvent(GtkWidget* widget, GdkEventMotion* event, FullscreenVideoController* controller) { controller->showHud(true); return TRUE; } static void onFullscreenGtkActiveNotification(GtkWidget* widget, GParamSpec* property, FullscreenVideoController* controller) { if (!gtk_window_is_active(GTK_WINDOW(widget))) controller->hideHud(); } static gboolean onFullscreenGtkConfigureEvent(GtkWidget* widget, GdkEventConfigure* event, FullscreenVideoController* controller) { controller->gtkConfigure(event); return TRUE; } static void onFullscreenGtkDestroy(GtkWidget* widget, FullscreenVideoController* controller) { controller->exitFullscreen(); } static void togglePlayPauseActivated(GtkAction* action, FullscreenVideoController* controller) { controller->togglePlay(); } static void exitFullscreenActivated(GtkAction* action, FullscreenVideoController* controller) { controller->exitOnUserRequest(); } static gboolean progressBarUpdateCallback(FullscreenVideoController* controller) { return controller->updateHudProgressBar(); } static gboolean timeScaleButtonPressed(GtkWidget* widget, GdkEventButton* event, FullscreenVideoController* controller) { if (event->type != GDK_BUTTON_PRESS) return FALSE; controller->beginSeek(); return FALSE; } static gboolean timeScaleButtonReleased(GtkWidget* widget, GdkEventButton* event, FullscreenVideoController* controller) { controller->endSeek(); return FALSE; } static void timeScaleValueChanged(GtkWidget* widget, FullscreenVideoController* controller) { controller->doSeek(); } static void volumeValueChanged(GtkScaleButton *button, gdouble value, FullscreenVideoController* controller) { controller->setVolume(static_cast(value)); } void playerVolumeChangedCallback(GObject *element, GParamSpec *pspec, FullscreenVideoController* controller) { controller->volumeChanged(); } void playerMuteChangedCallback(GObject *element, GParamSpec *pspec, FullscreenVideoController* controller) { controller->muteChanged(); } FullscreenVideoController::FullscreenVideoController() : m_hudTimeoutId(0) , m_progressBarUpdateId(0) , m_seekLock(false) , m_window(0) , m_hudWindow(0) { } FullscreenVideoController::~FullscreenVideoController() { exitFullscreen(); } void FullscreenVideoController::setMediaElement(HTMLMediaElement* mediaElement) { if (mediaElement == m_mediaElement) return; m_mediaElement = mediaElement; if (!m_mediaElement) { // Can't do full-screen, just get out exitFullscreen(); } } void FullscreenVideoController::gtkConfigure(GdkEventConfigure* event) { updateHudPosition(); } void FullscreenVideoController::showHud(bool autoHide) { if (!m_hudWindow) return; if (m_hudTimeoutId) { g_source_remove(m_hudTimeoutId); m_hudTimeoutId = 0; } // Show the cursor. GdkWindow* window = gtk_widget_get_window(m_window); gdk_window_set_cursor(window, 0); // Update the progress bar immediately before showing the window. updateHudProgressBar(); gtk_widget_show_all(m_hudWindow); updateHudPosition(); // Start periodic updates of the progress bar. if (!m_progressBarUpdateId) m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast(progressBarUpdateCallback), this); // Hide the hud in few seconds, if requested. if (autoHide) m_hudTimeoutId = g_timeout_add(HUD_AUTO_HIDE_INTERVAL, reinterpret_cast(hideHudCallback), this); } void FullscreenVideoController::hideHud() { if (m_hudTimeoutId) { g_source_remove(m_hudTimeoutId); m_hudTimeoutId = 0; } if (!m_hudWindow) return; // Keep the hud visible if a seek is in progress or if the volume // popup is visible. GtkWidget* volumePopup = gtk_scale_button_get_popup(GTK_SCALE_BUTTON(m_volumeButton)); if (m_seekLock || gtk_widget_get_visible(volumePopup)) { showHud(true); return; } GdkWindow* window = gtk_widget_get_window(m_window); GdkCursor* cursor = blankCursor(); gdk_window_set_cursor(window, cursor); gtk_widget_hide(m_hudWindow); if (m_progressBarUpdateId) { g_source_remove(m_progressBarUpdateId); m_progressBarUpdateId = 0; } } static gboolean onFullscreenGtkKeyPressEvent(GtkWidget* widget, GdkEventKey* event, FullscreenVideoController* controller) { switch (event->keyval) { case GDK_Escape: case 'f': case 'F': controller->exitOnUserRequest(); break; case GDK_space: case GDK_Return: controller->togglePlay(); break; case GDK_Up: // volume up controller->setVolume(controller->volume() + VOLUME_UP_OFFSET); break; case GDK_Down: // volume down controller->setVolume(controller->volume() - VOLUME_DOWN_OFFSET); break; default: break; } return TRUE; } void FullscreenVideoController::enterFullscreen() { if (!m_mediaElement) return; if (m_mediaElement->platformMedia().type != WebCore::PlatformMedia::GStreamerGWorldType) return; m_gstreamerGWorld = m_mediaElement->platformMedia().media.gstreamerGWorld; if (!m_gstreamerGWorld->enterFullscreen()) return; m_window = reinterpret_cast(m_gstreamerGWorld->platformVideoWindow()->window()); GstElement* pipeline = m_gstreamerGWorld->pipeline(); g_signal_connect(pipeline, "notify::volume", G_CALLBACK(playerVolumeChangedCallback), this); g_signal_connect(pipeline, "notify::mute", G_CALLBACK(playerMuteChangedCallback), this); if (!m_hudWindow) createHud(); // Ensure black background. #ifdef GTK_API_VERSION_2 GdkColor color = { 1, 0, 0, 0 }; gtk_widget_modify_bg(m_window, GTK_STATE_NORMAL, &color); #else GdkRGBA color = { 0, 0, 0, 1}; gtk_widget_override_background_color(m_window, GTK_STATE_FLAG_NORMAL, &color); #endif gtk_widget_set_double_buffered(m_window, FALSE); g_signal_connect(m_window, "key-press-event", G_CALLBACK(onFullscreenGtkKeyPressEvent), this); g_signal_connect(m_window, "destroy", G_CALLBACK(onFullscreenGtkDestroy), this); g_signal_connect(m_window, "notify::is-active", G_CALLBACK(onFullscreenGtkActiveNotification), this); gtk_widget_show_all(m_window); GdkWindow* window = gtk_widget_get_window(m_window); GRefPtr cursor(adoptGRef(blankCursor())); gdk_window_set_cursor(window, cursor.get()); g_signal_connect(m_window, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this); g_signal_connect(m_window, "configure-event", G_CALLBACK(onFullscreenGtkConfigureEvent), this); gtk_window_fullscreen(GTK_WINDOW(m_window)); showHud(true); } void FullscreenVideoController::updateHudPosition() { if (!m_hudWindow) return; // Get the screen rectangle. GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_window)); GdkWindow* window = gtk_widget_get_window(m_window); GdkRectangle fullscreenRectangle; gdk_screen_get_monitor_geometry(screen, gdk_screen_get_monitor_at_window(screen, window), &fullscreenRectangle); // Get the popup window size. int hudWidth, hudHeight; gtk_window_get_size(GTK_WINDOW(m_hudWindow), &hudWidth, &hudHeight); // Resize the hud to the full width of the screen. gtk_window_resize(GTK_WINDOW(m_hudWindow), fullscreenRectangle.width, hudHeight); // Move the hud to the bottom of the screen. gtk_window_move(GTK_WINDOW(m_hudWindow), fullscreenRectangle.x, fullscreenRectangle.height + fullscreenRectangle.y - hudHeight); } void FullscreenVideoController::exitOnUserRequest() { m_mediaElement->exitFullscreen(); } void FullscreenVideoController::exitFullscreen() { if (!m_hudWindow) return; g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast(onFullscreenGtkKeyPressEvent), this); g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast(onFullscreenGtkDestroy), this); g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast(onFullscreenGtkMotionNotifyEvent), this); g_signal_handlers_disconnect_by_func(m_window, reinterpret_cast(onFullscreenGtkConfigureEvent), this); GstElement* pipeline = m_mediaElement->platformMedia().media.gstreamerGWorld->pipeline(); g_signal_handlers_disconnect_by_func(pipeline, reinterpret_cast(playerVolumeChangedCallback), this); g_signal_handlers_disconnect_by_func(pipeline, reinterpret_cast(playerMuteChangedCallback), this); if (m_hudTimeoutId) { g_source_remove(m_hudTimeoutId); m_hudTimeoutId = 0; } if (m_progressBarUpdateId) { g_source_remove(m_progressBarUpdateId); m_progressBarUpdateId = 0; } if (m_mediaElement->platformMedia().type == WebCore::PlatformMedia::GStreamerGWorldType) m_mediaElement->platformMedia().media.gstreamerGWorld->exitFullscreen(); gtk_widget_hide(m_window); gtk_widget_destroy(m_hudWindow); m_hudWindow = 0; } bool FullscreenVideoController::canPlay() const { return m_mediaElement && m_mediaElement->canPlay(); } void FullscreenVideoController::play() { if (m_mediaElement) m_mediaElement->play(m_mediaElement->processingUserGesture()); playStateChanged(); showHud(true); } void FullscreenVideoController::pause() { if (m_mediaElement) m_mediaElement->pause(m_mediaElement->processingUserGesture()); playStateChanged(); showHud(false); } void FullscreenVideoController::playStateChanged() { if (canPlay()) g_object_set(m_playPauseAction, "tooltip", _("Play"), "icon-name", PLAY_ICON_NAME, NULL); else g_object_set(m_playPauseAction, "tooltip", _("Pause"), "icon-name", PAUSE_ICON_NAME, NULL); } void FullscreenVideoController::togglePlay() { if (canPlay()) play(); else pause(); } float FullscreenVideoController::volume() const { return m_mediaElement ? m_mediaElement->volume() : 0; } bool FullscreenVideoController::muted() const { return m_mediaElement ? m_mediaElement->muted() : false; } void FullscreenVideoController::setVolume(float volume) { if (volume < 0.0 || volume > 1.0) return; if (m_mediaElement) { ExceptionCode ec; m_mediaElement->setVolume(volume, ec); } } void FullscreenVideoController::volumeChanged() { g_signal_handler_block(m_volumeButton, m_volumeUpdateId); gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), volume()); g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId); } void FullscreenVideoController::muteChanged() { g_signal_handler_block(m_volumeButton, m_volumeUpdateId); gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), muted() ? 0 : volume()); g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId); } float FullscreenVideoController::currentTime() const { return m_mediaElement ? m_mediaElement->currentTime() : 0; } void FullscreenVideoController::setCurrentTime(float value) { if (m_mediaElement) { ExceptionCode ec; m_mediaElement->setCurrentTime(value, ec); } } float FullscreenVideoController::duration() const { return m_mediaElement ? m_mediaElement->duration() : 0; } float FullscreenVideoController::percentLoaded() const { return m_mediaElement ? m_mediaElement->percentLoaded() : 0; } void FullscreenVideoController::beginSeek() { m_seekLock = true; if (m_mediaElement) m_mediaElement->beginScrubbing(); } void FullscreenVideoController::doSeek() { if (!m_seekLock) return; setCurrentTime(gtk_range_get_value(GTK_RANGE(m_timeHScale))*duration() / 100); } void FullscreenVideoController::endSeek() { if (m_mediaElement) m_mediaElement->endScrubbing(); m_seekLock = false; } static String timeToString(float time) { if (!isfinite(time)) time = 0; int seconds = fabsf(time); int hours = seconds / (60 * 60); int minutes = (seconds / 60) % 60; seconds %= 60; if (hours) { if (hours > 9) return String::format("%s%02d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds); return String::format("%s%01d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds); } return String::format("%s%02d:%02d", (time < 0 ? "-" : ""), minutes, seconds); } gboolean FullscreenVideoController::updateHudProgressBar() { float mediaDuration(duration()); float mediaPosition(currentTime()); if (!m_seekLock) { gdouble value = 0.0; if (mediaPosition && mediaDuration) value = (mediaPosition * 100.0) / mediaDuration; GtkAdjustment* adjustment = gtk_range_get_adjustment(GTK_RANGE(m_timeHScale)); gtk_adjustment_set_value(adjustment, value); } gtk_range_set_fill_level(GTK_RANGE(m_timeHScale), percentLoaded()* 100); gchar* label = g_strdup_printf("%s / %s", timeToString(mediaPosition).utf8().data(), timeToString(mediaDuration).utf8().data()); gtk_label_set_text(GTK_LABEL(m_timeLabel), label); g_free(label); return TRUE; } void FullscreenVideoController::createHud() { m_hudWindow = gtk_window_new(GTK_WINDOW_POPUP); gtk_window_set_gravity(GTK_WINDOW(m_hudWindow), GDK_GRAVITY_SOUTH_WEST); gtk_window_set_type_hint(GTK_WINDOW(m_hudWindow), GDK_WINDOW_TYPE_HINT_NORMAL); g_signal_connect(m_hudWindow, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this); GtkWidget* hbox = gtk_hbox_new(FALSE, 4); gtk_container_add(GTK_CONTAINER(m_hudWindow), hbox); m_playPauseAction = gtk_action_new("play", _("Play / Pause"), _("Play or pause the media"), PAUSE_ICON_NAME); g_signal_connect(m_playPauseAction, "activate", G_CALLBACK(togglePlayPauseActivated), this); playStateChanged(); GtkWidget* item = gtk_action_create_tool_item(m_playPauseAction); gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0); GtkWidget* label = gtk_label_new(_("Time:")); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, TRUE, 0); GtkAdjustment* adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 100.0, 0.1, 1.0, 1.0)); m_timeHScale = gtk_hscale_new(adjustment); gtk_scale_set_draw_value(GTK_SCALE(m_timeHScale), FALSE); gtk_range_set_show_fill_level(GTK_RANGE(m_timeHScale), TRUE); g_signal_connect(m_timeHScale, "button-press-event", G_CALLBACK(timeScaleButtonPressed), this); g_signal_connect(m_timeHScale, "button-release-event", G_CALLBACK(timeScaleButtonReleased), this); m_hscaleUpdateId = g_signal_connect(m_timeHScale, "value-changed", G_CALLBACK(timeScaleValueChanged), this); gtk_box_pack_start(GTK_BOX(hbox), m_timeHScale, TRUE, TRUE, 0); m_timeLabel = gtk_label_new(""); gtk_box_pack_start(GTK_BOX(hbox), m_timeLabel, FALSE, TRUE, 0); // Volume button. m_volumeButton = gtk_volume_button_new(); gtk_box_pack_start(GTK_BOX(hbox), m_volumeButton, FALSE, TRUE, 0); gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), volume()); m_volumeUpdateId = g_signal_connect(m_volumeButton, "value-changed", G_CALLBACK(volumeValueChanged), this); m_exitFullscreenAction = gtk_action_new("exit", _("Exit Fullscreen"), _("Exit from fullscreen mode"), EXIT_FULLSCREEN_ICON_NAME); g_signal_connect(m_exitFullscreenAction, "activate", G_CALLBACK(exitFullscreenActivated), this); g_object_set(m_exitFullscreenAction, "icon-name", EXIT_FULLSCREEN_ICON_NAME, NULL); item = gtk_action_create_tool_item(m_exitFullscreenAction); gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0); m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast(progressBarUpdateCallback), this); } #endif