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