1/*
2 * Copyright (C) 2009 Apple Inc.
3 * Copyright (C) 2009 Google Inc.
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
16 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
19 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#include "config.h"
29#include "core/rendering/RenderMediaControls.h"
30
31#include "bindings/core/v8/ExceptionStatePlaceholder.h"
32#include "core/html/HTMLMediaElement.h"
33#include "core/html/TimeRanges.h"
34#include "core/rendering/PaintInfo.h"
35#include "platform/graphics/Gradient.h"
36#include "platform/graphics/GraphicsContext.h"
37
38namespace blink {
39
40typedef WTF::HashMap<const char*, Image*> MediaControlImageMap;
41static MediaControlImageMap* gMediaControlImageMap = 0;
42
43static Image* platformResource(const char* name)
44{
45    if (!gMediaControlImageMap)
46        gMediaControlImageMap = new MediaControlImageMap();
47    if (Image* image = gMediaControlImageMap->get(name))
48        return image;
49    if (Image* image = Image::loadPlatformResource(name).leakRef()) {
50        gMediaControlImageMap->set(name, image);
51        return image;
52    }
53    ASSERT_NOT_REACHED();
54    return 0;
55}
56
57static bool hasSource(const HTMLMediaElement* mediaElement)
58{
59    return mediaElement->networkState() != HTMLMediaElement::NETWORK_EMPTY
60        && mediaElement->networkState() != HTMLMediaElement::NETWORK_NO_SOURCE;
61}
62
63static bool paintMediaButton(GraphicsContext* context, const IntRect& rect, Image* image)
64{
65    context->drawImage(image, rect);
66    return true;
67}
68
69static bool paintMediaMuteButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
70{
71    HTMLMediaElement* mediaElement = toParentMediaElement(object);
72    if (!mediaElement)
73        return false;
74
75    static Image* soundLevel3 = platformResource("mediaplayerSoundLevel3");
76    static Image* soundLevel2 = platformResource("mediaplayerSoundLevel2");
77    static Image* soundLevel1 = platformResource("mediaplayerSoundLevel1");
78    static Image* soundLevel0 = platformResource("mediaplayerSoundLevel0");
79    static Image* soundDisabled = platformResource("mediaplayerSoundDisabled");
80
81    if (!hasSource(mediaElement) || !mediaElement->hasAudio())
82        return paintMediaButton(paintInfo.context, rect, soundDisabled);
83
84    if (mediaElement->muted() || mediaElement->volume() <= 0)
85        return paintMediaButton(paintInfo.context, rect, soundLevel0);
86
87    if (mediaElement->volume() <= 0.33)
88        return paintMediaButton(paintInfo.context, rect, soundLevel1);
89
90    if (mediaElement->volume() <= 0.66)
91        return paintMediaButton(paintInfo.context, rect, soundLevel2);
92
93    return paintMediaButton(paintInfo.context, rect, soundLevel3);
94}
95
96static bool paintMediaPlayButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
97{
98    HTMLMediaElement* mediaElement = toParentMediaElement(object);
99    if (!mediaElement)
100        return false;
101
102    static Image* mediaPlay = platformResource("mediaplayerPlay");
103    static Image* mediaPause = platformResource("mediaplayerPause");
104    static Image* mediaPlayDisabled = platformResource("mediaplayerPlayDisabled");
105
106    if (!hasSource(mediaElement))
107        return paintMediaButton(paintInfo.context, rect, mediaPlayDisabled);
108
109    return paintMediaButton(paintInfo.context, rect, mediaControlElementType(object->node()) == MediaPlayButton ? mediaPlay : mediaPause);
110}
111
112static bool paintMediaOverlayPlayButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
113{
114    HTMLMediaElement* mediaElement = toParentMediaElement(object);
115    if (!mediaElement)
116        return false;
117
118    if (!hasSource(mediaElement) || !mediaElement->togglePlayStateWillPlay())
119        return false;
120
121    static Image* mediaOverlayPlay = platformResource("mediaplayerOverlayPlay");
122    return paintMediaButton(paintInfo.context, rect, mediaOverlayPlay);
123}
124
125static Image* getMediaSliderThumb()
126{
127    static Image* mediaSliderThumb = platformResource("mediaplayerSliderThumb");
128    return mediaSliderThumb;
129}
130
131static void paintRoundedSliderBackground(const IntRect& rect, const RenderStyle* style, GraphicsContext* context)
132{
133    int borderRadius = rect.height() / 2;
134    IntSize radii(borderRadius, borderRadius);
135    Color sliderBackgroundColor = Color(11, 11, 11);
136    context->fillRoundedRect(rect, radii, radii, radii, radii, sliderBackgroundColor);
137}
138
139static void paintSliderRangeHighlight(const IntRect& rect, const RenderStyle* style, GraphicsContext* context, int startPosition, int endPosition, Color startColor, Color endColor)
140{
141    // Calculate border radius; need to avoid being smaller than half the slider height
142    // because of https://bugs.webkit.org/show_bug.cgi?id=30143.
143    int borderRadius = rect.height() / 2;
144    IntSize radii(borderRadius, borderRadius);
145
146    // Calculate highlight rectangle and edge dimensions.
147    int startOffset = startPosition;
148    int endOffset = rect.width() - endPosition;
149    int rangeWidth = endPosition - startPosition;
150
151    if (rangeWidth <= 0)
152        return;
153
154    // Make sure the range width is bigger than border radius at the edges to retain rounded corners.
155    if (startOffset < borderRadius && rangeWidth < borderRadius)
156        rangeWidth = borderRadius;
157    if (endOffset < borderRadius && rangeWidth < borderRadius)
158        rangeWidth = borderRadius;
159
160    // Set rectangle to highlight range.
161    IntRect highlightRect = rect;
162    highlightRect.move(startOffset, 0);
163    highlightRect.setWidth(rangeWidth);
164
165    // Don't bother drawing an empty area.
166    if (highlightRect.isEmpty())
167        return;
168
169    // Calculate white-grey gradient.
170    IntPoint sliderTopLeft = highlightRect.location();
171    IntPoint sliderBottomLeft = sliderTopLeft;
172    sliderBottomLeft.move(0, highlightRect.height());
173    RefPtr<Gradient> gradient = Gradient::create(sliderTopLeft, sliderBottomLeft);
174    gradient->addColorStop(0.0, startColor);
175    gradient->addColorStop(1.0, endColor);
176
177    // Fill highlight rectangle with gradient, potentially rounded if on left or right edge.
178    context->save();
179    context->setFillGradient(gradient);
180
181    if (startOffset < borderRadius && endOffset < borderRadius)
182        context->fillRoundedRect(highlightRect, radii, radii, radii, radii, startColor);
183    else if (startOffset < borderRadius)
184        context->fillRoundedRect(highlightRect, radii, IntSize(0, 0), radii, IntSize(0, 0), startColor);
185    else if (endOffset < borderRadius)
186        context->fillRoundedRect(highlightRect, IntSize(0, 0), radii, IntSize(0, 0), radii, startColor);
187    else
188        context->fillRect(highlightRect);
189
190    context->restore();
191}
192
193const int mediaSliderThumbWidth = 32;
194
195static bool paintMediaSlider(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
196{
197    HTMLMediaElement* mediaElement = toParentMediaElement(object);
198    if (!mediaElement)
199        return false;
200
201    RenderStyle* style = object->style();
202    GraphicsContext* context = paintInfo.context;
203
204    paintRoundedSliderBackground(rect, style, context);
205
206    // Draw the buffered range. Since the element may have multiple buffered ranges and it'd be
207    // distracting/'busy' to show all of them, show only the buffered range containing the current play head.
208    RefPtrWillBeRawPtr<TimeRanges> bufferedTimeRanges = mediaElement->buffered();
209    float duration = mediaElement->duration();
210    float currentTime = mediaElement->currentTime();
211    if (std::isnan(duration) || std::isinf(duration) || !duration || std::isnan(currentTime))
212        return true;
213
214    for (unsigned i = 0; i < bufferedTimeRanges->length(); ++i) {
215        float start = bufferedTimeRanges->start(i, ASSERT_NO_EXCEPTION);
216        float end = bufferedTimeRanges->end(i, ASSERT_NO_EXCEPTION);
217        if (std::isnan(start) || std::isnan(end) || start > currentTime || end < currentTime)
218            continue;
219        int startPosition = int(start * rect.width() / duration);
220        int currentPosition = int(currentTime * rect.width() / duration);
221        int endPosition = int(end * rect.width() / duration);
222
223        // Add half the thumb width proportionally adjusted to the current painting position.
224        int thumbCenter = mediaSliderThumbWidth / 2;
225        int addWidth = thumbCenter * (1.0 - 2.0 * currentPosition / rect.width());
226        currentPosition += addWidth;
227
228        // Draw white-ish highlight before current time.
229        Color startColor = Color(195, 195, 195);
230        Color endColor = Color(217, 217, 217);
231        if (currentPosition > startPosition)
232            paintSliderRangeHighlight(rect, style, context, startPosition, currentPosition, startColor, endColor);
233
234        // Draw grey-ish highlight after current time.
235        startColor = Color(60, 60, 60);
236        endColor = Color(76, 76, 76);
237
238        if (endPosition > currentPosition)
239            paintSliderRangeHighlight(rect, style, context, currentPosition, endPosition, startColor, endColor);
240
241        return true;
242    }
243
244    return true;
245}
246
247static bool paintMediaSliderThumb(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
248{
249    if (!object->node())
250        return false;
251
252    HTMLMediaElement* mediaElement = toParentMediaElement(object->node()->shadowHost());
253    if (!mediaElement)
254        return false;
255
256    if (!hasSource(mediaElement))
257        return true;
258
259    Image* mediaSliderThumb = getMediaSliderThumb();
260    return paintMediaButton(paintInfo.context, rect, mediaSliderThumb);
261}
262
263const int mediaVolumeSliderThumbWidth = 24;
264
265static bool paintMediaVolumeSlider(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
266{
267    HTMLMediaElement* mediaElement = toParentMediaElement(object);
268    if (!mediaElement)
269        return false;
270
271    GraphicsContext* context = paintInfo.context;
272    RenderStyle* style = object->style();
273
274    paintRoundedSliderBackground(rect, style, context);
275
276    // Calculate volume position for white background rectangle.
277    float volume = mediaElement->volume();
278    if (std::isnan(volume) || volume < 0)
279        return true;
280    if (volume > 1)
281        volume = 1;
282    if (!hasSource(mediaElement) || !mediaElement->hasAudio() || mediaElement->muted())
283        volume = 0;
284
285    // Calculate the position relative to the center of the thumb.
286    float fillWidth = 0;
287    if (volume > 0) {
288        float thumbCenter = mediaVolumeSliderThumbWidth / 2;
289        float zoomLevel = style->effectiveZoom();
290        float positionWidth = volume * (rect.width() - (zoomLevel * thumbCenter));
291        fillWidth = positionWidth + (zoomLevel * thumbCenter / 2);
292    }
293
294    Color startColor = Color(195, 195, 195);
295    Color endColor = Color(217, 217, 217);
296
297    paintSliderRangeHighlight(rect, style, context, 0.0, fillWidth, startColor, endColor);
298
299    return true;
300}
301
302static bool paintMediaVolumeSliderThumb(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
303{
304    if (!object->node())
305        return false;
306
307    HTMLMediaElement* mediaElement = toParentMediaElement(object->node()->shadowHost());
308    if (!mediaElement)
309        return false;
310
311    if (!hasSource(mediaElement) || !mediaElement->hasAudio())
312        return true;
313
314    static Image* mediaVolumeSliderThumb = platformResource("mediaplayerVolumeSliderThumb");
315    return paintMediaButton(paintInfo.context, rect, mediaVolumeSliderThumb);
316}
317
318static bool paintMediaFullscreenButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
319{
320    HTMLMediaElement* mediaElement = toParentMediaElement(object);
321    if (!mediaElement)
322        return false;
323
324    static Image* mediaFullscreenButton = platformResource("mediaplayerFullscreen");
325    return paintMediaButton(paintInfo.context, rect, mediaFullscreenButton);
326}
327
328static bool paintMediaToggleClosedCaptionsButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
329{
330    HTMLMediaElement* mediaElement = toParentMediaElement(object);
331    if (!mediaElement)
332        return false;
333
334    static Image* mediaClosedCaptionButton = platformResource("mediaplayerClosedCaption");
335    static Image* mediaClosedCaptionButtonDisabled = platformResource("mediaplayerClosedCaptionDisabled");
336
337    if (mediaElement->closedCaptionsVisible())
338        return paintMediaButton(paintInfo.context, rect, mediaClosedCaptionButton);
339
340    return paintMediaButton(paintInfo.context, rect, mediaClosedCaptionButtonDisabled);
341}
342static bool paintMediaCastButton(RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
343{
344    HTMLMediaElement* mediaElement = toParentMediaElement(object);
345    if (!mediaElement)
346        return false;
347
348    static Image* mediaCastOnButton = platformResource("mediaplayerCastOn");
349    static Image* mediaCastOffButton = platformResource("mediaplayerCastOff");
350
351    if (mediaElement->isPlayingRemotely()) {
352        return paintMediaButton(paintInfo.context, rect, mediaCastOnButton);
353    }
354
355    return paintMediaButton(paintInfo.context, rect, mediaCastOffButton);
356
357}
358
359bool RenderMediaControls::paintMediaControlsPart(MediaControlElementType part, RenderObject* object, const PaintInfo& paintInfo, const IntRect& rect)
360{
361    switch (part) {
362    case MediaMuteButton:
363    case MediaUnMuteButton:
364        return paintMediaMuteButton(object, paintInfo, rect);
365    case MediaPauseButton:
366    case MediaPlayButton:
367        return paintMediaPlayButton(object, paintInfo, rect);
368    case MediaShowClosedCaptionsButton:
369        return paintMediaToggleClosedCaptionsButton(object, paintInfo, rect);
370    case MediaSlider:
371        return paintMediaSlider(object, paintInfo, rect);
372    case MediaSliderThumb:
373        return paintMediaSliderThumb(object, paintInfo, rect);
374    case MediaVolumeSlider:
375        return paintMediaVolumeSlider(object, paintInfo, rect);
376    case MediaVolumeSliderThumb:
377        return paintMediaVolumeSliderThumb(object, paintInfo, rect);
378    case MediaEnterFullscreenButton:
379    case MediaExitFullscreenButton:
380        return paintMediaFullscreenButton(object, paintInfo, rect);
381    case MediaOverlayPlayButton:
382        return paintMediaOverlayPlayButton(object, paintInfo, rect);
383    case MediaCastOffButton:
384    case MediaCastOnButton:
385    case MediaOverlayCastOffButton:
386    case MediaOverlayCastOnButton:
387        return paintMediaCastButton(object, paintInfo, rect);
388    case MediaVolumeSliderContainer:
389    case MediaTimelineContainer:
390    case MediaCurrentTimeDisplay:
391    case MediaTimeRemainingDisplay:
392    case MediaControlsPanel:
393    case MediaStatusDisplay:
394    case MediaHideClosedCaptionsButton:
395    case MediaTextTrackDisplayContainer:
396    case MediaTextTrackDisplay:
397    case MediaFullScreenVolumeSlider:
398    case MediaFullScreenVolumeSliderThumb:
399        ASSERT_NOT_REACHED();
400        break;
401    }
402    return false;
403}
404
405const int mediaSliderThumbHeight = 24;
406const int mediaVolumeSliderThumbHeight = 24;
407
408void RenderMediaControls::adjustMediaSliderThumbSize(RenderStyle* style)
409{
410    static Image* mediaSliderThumb = platformResource("mediaplayerSliderThumb");
411    static Image* mediaVolumeSliderThumb = platformResource("mediaplayerVolumeSliderThumb");
412    int width = 0;
413    int height = 0;
414
415    Image* thumbImage = 0;
416    if (style->appearance() == MediaSliderThumbPart) {
417        thumbImage = mediaSliderThumb;
418        width = mediaSliderThumbWidth;
419        height = mediaSliderThumbHeight;
420    } else if (style->appearance() == MediaVolumeSliderThumbPart) {
421        thumbImage = mediaVolumeSliderThumb;
422        width = mediaVolumeSliderThumbWidth;
423        height = mediaVolumeSliderThumbHeight;
424    }
425
426    float zoomLevel = style->effectiveZoom();
427    if (thumbImage) {
428        style->setWidth(Length(static_cast<int>(width * zoomLevel), Fixed));
429        style->setHeight(Length(static_cast<int>(height * zoomLevel), Fixed));
430    }
431}
432
433static String formatChromiumMediaControlsTime(float time, float duration)
434{
435    if (!std::isfinite(time))
436        time = 0;
437    if (!std::isfinite(duration))
438        duration = 0;
439    int seconds = static_cast<int>(fabsf(time));
440    int hours = seconds / (60 * 60);
441    int minutes = (seconds / 60) % 60;
442    seconds %= 60;
443
444    // duration defines the format of how the time is rendered
445    int durationSecs = static_cast<int>(fabsf(duration));
446    int durationHours = durationSecs / (60 * 60);
447    int durationMins = (durationSecs / 60) % 60;
448
449    if (durationHours || hours)
450        return String::format("%s%01d:%02d:%02d", (time < 0 ? "-" : ""), hours, minutes, seconds);
451    if (durationMins > 9)
452        return String::format("%s%02d:%02d", (time < 0 ? "-" : ""), minutes, seconds);
453
454    return String::format("%s%01d:%02d", (time < 0 ? "-" : ""), minutes, seconds);
455}
456
457String RenderMediaControls::formatMediaControlsTime(float time)
458{
459    return formatChromiumMediaControlsTime(time, time);
460}
461
462String RenderMediaControls::formatMediaControlsCurrentTime(float currentTime, float duration)
463{
464    return formatChromiumMediaControlsTime(currentTime, duration);
465}
466
467} // namespace blink
468