RenderMedia.cpp   [plain text]


/*
 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"

#if ENABLE(VIDEO)
#include "RenderMedia.h"

#include "CSSStyleSelector.h"
#include "Event.h"
#include "EventNames.h"
#include "FloatConversion.h"
#include "FrameView.h"
#include "GraphicsContext.h"
#include "HTMLMediaElement.h"
#include "MediaControlElements.h"
#include "MouseEvent.h"
#include "MediaPlayer.h"
#include "RenderSlider.h"
#include "SystemTime.h"
#include <wtf/MathExtras.h>

using namespace std;

namespace WebCore {

using namespace EventNames;

static const double cTimeUpdateRepeatDelay = 0.2;
static const double cOpacityAnimationRepeatDelay = 0.05;
// FIXME get this from style
static const double cOpacityAnimationDuration = 0.1;

RenderMedia::RenderMedia(HTMLMediaElement* video)
    : RenderReplaced(video)
    , m_timeUpdateTimer(this, &RenderMedia::timeUpdateTimerFired)
    , m_opacityAnimationTimer(this, &RenderMedia::opacityAnimationTimerFired)
    , m_mouseOver(false)
    , m_opacityAnimationStartTime(0)
    , m_opacityAnimationFrom(0)
    , m_opacityAnimationTo(1.0f)
{
}

RenderMedia::RenderMedia(HTMLMediaElement* video, const IntSize& intrinsicSize)
    : RenderReplaced(video, intrinsicSize)
    , m_timeUpdateTimer(this, &RenderMedia::timeUpdateTimerFired)
    , m_opacityAnimationTimer(this, &RenderMedia::opacityAnimationTimerFired)
    , m_mouseOver(false)
    , m_opacityAnimationStartTime(0)
    , m_opacityAnimationFrom(0)
    , m_opacityAnimationTo(1.0f)
{
}

RenderMedia::~RenderMedia()
{
    if (m_controlsShadowRoot && m_controlsShadowRoot->renderer()) {
        static_cast<RenderMediaControlShadowRoot*>(m_controlsShadowRoot->renderer())->setParent(0);
        m_controlsShadowRoot->detach();
    }
}
 
HTMLMediaElement* RenderMedia::mediaElement() const
{ 
    return static_cast<HTMLMediaElement*>(node()); 
}

MediaPlayer* RenderMedia::player() const
{
    return mediaElement()->player();
}

void RenderMedia::layout()
{
    IntSize oldSize = contentBox().size();

    RenderReplaced::layout();

    RenderObject* controlsRenderer = m_controlsShadowRoot ? m_controlsShadowRoot->renderer() : 0;
    if (!controlsRenderer)
        return;
    IntSize newSize = contentBox().size();
    if (newSize != oldSize || controlsRenderer->needsLayout()) {
        controlsRenderer->setPos(borderLeft() + paddingLeft(), borderTop() + paddingTop());
        controlsRenderer->style()->setHeight(Length(newSize.height(), Fixed));
        controlsRenderer->style()->setWidth(Length(newSize.width(), Fixed));
        controlsRenderer->setNeedsLayout(true, false);
        controlsRenderer->layout();
        setChildNeedsLayout(false);
    }
}

RenderObject* RenderMedia::firstChild() const 
{ 
    return m_controlsShadowRoot ? m_controlsShadowRoot->renderer() : 0; 
}

RenderObject* RenderMedia::lastChild() const 
{ 
    return m_controlsShadowRoot ? m_controlsShadowRoot->renderer() : 0;
}
    
void RenderMedia::removeChild(RenderObject* child)
{
    ASSERT(m_controlsShadowRoot);
    ASSERT(child == m_controlsShadowRoot->renderer());
    child->removeLayers(enclosingLayer());
}
    
void RenderMedia::createControlsShadowRoot()
{
    ASSERT(!m_controlsShadowRoot);
    m_controlsShadowRoot = new MediaControlShadowRootElement(document(), mediaElement());
}

void RenderMedia::createPanel()
{
    ASSERT(!m_panel);
    RenderStyle* style = getPseudoStyle(RenderStyle::MEDIA_CONTROLS_PANEL);
    m_panel = new HTMLDivElement(document());
    RenderObject* renderer = m_panel->createRenderer(renderArena(), style);
    if (renderer) {
        m_panel->setRenderer(renderer);
        renderer->setStyle(style);
        m_panel->setAttached();
        m_panel->setInDocument(true);
        m_controlsShadowRoot->addChild(m_panel);
        m_controlsShadowRoot->renderer()->addChild(renderer);
    }
}

void RenderMedia::createMuteButton()
{
    ASSERT(!m_muteButton);
    m_muteButton = new MediaControlMuteButtonElement(document(), mediaElement());
    m_muteButton->attachToParent(m_panel.get());
}

void RenderMedia::createPlayButton()
{
    ASSERT(!m_playButton);
    m_playButton = new MediaControlPlayButtonElement(document(), mediaElement());
    m_playButton->attachToParent(m_panel.get());
}

void RenderMedia::createSeekBackButton()
{
    ASSERT(!m_seekBackButton);
    m_seekBackButton = new MediaControlSeekButtonElement(document(), mediaElement(), false);
    m_seekBackButton->attachToParent(m_panel.get());
}

void RenderMedia::createSeekForwardButton()
{
    ASSERT(!m_seekForwardButton);
    m_seekForwardButton = new MediaControlSeekButtonElement(document(), mediaElement(), true);
    m_seekForwardButton->attachToParent(m_panel.get());
}

void RenderMedia::createTimeline()
{
    ASSERT(!m_timeline);
    m_timeline = new MediaControlTimelineElement(document(), mediaElement());
    m_timeline->attachToParent(m_panel.get());
}
  
void RenderMedia::createTimeDisplay()
{
    ASSERT(!m_timeDisplay);
    RenderStyle* style = getPseudoStyle(RenderStyle::MEDIA_CONTROLS_TIME_DISPLAY);
    m_timeDisplay = new HTMLDivElement(document());
    RenderObject* renderer = m_timeDisplay->createRenderer(renderArena(), style);
    if (renderer) {
        m_timeDisplay->setRenderer(renderer);
        renderer->setStyle(style);
        m_timeDisplay->setAttached();
        m_timeDisplay->setInDocument(true);
        m_panel->addChild(m_timeDisplay);
        m_panel->renderer()->addChild(renderer);
    }
}

void RenderMedia::createFullscreenButton()
{
    ASSERT(!m_fullscreenButton);
    m_fullscreenButton = new MediaControlFullscreenButtonElement(document(), mediaElement());
    m_fullscreenButton->attachToParent(m_panel.get());
}
    
void RenderMedia::updateFromElement()
{
    updateControls();
}
            
void RenderMedia::updateControls()
{
    HTMLMediaElement* media = mediaElement();
    if (!media->controls() || media->inPageCache()) {
        if (m_controlsShadowRoot) {
            m_controlsShadowRoot->detach();
            m_panel = 0;
            m_muteButton = 0;
            m_playButton = 0;
            m_timeline = 0;
            m_seekBackButton = 0;
            m_seekForwardButton = 0;
            m_timeDisplay = 0;
            m_fullscreenButton = 0;
            m_controlsShadowRoot = 0;
        }
        m_opacityAnimationTo = 1.0f;
        m_opacityAnimationTimer.stop();
        m_timeUpdateTimer.stop();
        return;
    }
    
    if (!m_controlsShadowRoot) {
        createControlsShadowRoot();
        createPanel();
        createMuteButton();
        createPlayButton();
        createTimeline();
        createSeekBackButton();
        createSeekForwardButton();
        createTimeDisplay();
        createFullscreenButton();
    }
    
    if (media->paused() || media->ended() || media->networkState() < HTMLMediaElement::LOADED_METADATA)
        m_timeUpdateTimer.stop();
    else
        m_timeUpdateTimer.startRepeating(cTimeUpdateRepeatDelay);
    
    if (m_muteButton)
        m_muteButton->update();
    if (m_playButton)
        m_playButton->update();
    if (m_timeline)
        m_timeline->update();
    if (m_seekBackButton)
        m_seekBackButton->update();
    if (m_seekForwardButton)
        m_seekForwardButton->update();
    if (m_fullscreenButton)
        m_fullscreenButton->update();
    updateTimeDisplay();
    updateControlVisibility();
}

void RenderMedia::timeUpdateTimerFired(Timer<RenderMedia>*)
{
    if (m_timeline)
        m_timeline->update(false);
    updateTimeDisplay();
}
    
String RenderMedia::formatTime(float time)
{
    if (!isfinite(time))
        time = 0;
    int seconds = (int)time; 
    int hours = seconds / (60 * 60);
    int minutes = (seconds / 60) % 60;
    seconds %= 60;
    return String::format("%02d:%02d:%02d", hours, minutes, seconds);
}

void RenderMedia::updateTimeDisplay()
{
    if (!m_timeDisplay)
        return;
    String timeString = formatTime(mediaElement()->currentTime());
    ExceptionCode ec;
    m_timeDisplay->setInnerText(timeString, ec);
}
    
void RenderMedia::updateControlVisibility() 
{
    if (!m_panel || !m_panel->renderer())
        return;
    // do fading manually, css animations don't work well with shadow trees
    HTMLMediaElement* media = mediaElement();
    // Don't fade for audio controls.
    if (!media->isVideo())
        return;
    bool visible = m_mouseOver || media->paused() || media->ended() || media->networkState() < HTMLMediaElement::LOADED_METADATA;
    if (visible == (m_opacityAnimationTo > 0))
        return;
    if (visible) {
        m_opacityAnimationFrom = m_panel->renderer()->style()->opacity();
        m_opacityAnimationTo = 1.0f;
    } else {
        m_opacityAnimationFrom = m_panel->renderer()->style()->opacity();
        m_opacityAnimationTo = 0;
    }
    m_opacityAnimationStartTime = currentTime();
    m_opacityAnimationTimer.startRepeating(cOpacityAnimationRepeatDelay);
}
    
void RenderMedia::changeOpacity(HTMLElement* e, float opacity) 
{
    if (!e || !e->renderer() || !e->renderer()->style())
        return;
    RenderStyle* s = new (renderArena()) RenderStyle(*e->renderer()->style());
    s->setOpacity(opacity);
    // z-index can't be auto if opacity is used
    s->setZIndex(0);
    e->renderer()->setStyle(s);
}
    
void RenderMedia::opacityAnimationTimerFired(Timer<RenderMedia>*)
{
    double time = currentTime() - m_opacityAnimationStartTime;
    if (time >= cOpacityAnimationDuration) {
        time = cOpacityAnimationDuration;
        m_opacityAnimationTimer.stop();
    }
    float opacity = narrowPrecisionToFloat(m_opacityAnimationFrom + (m_opacityAnimationTo - m_opacityAnimationFrom) * time / cOpacityAnimationDuration);
    changeOpacity(m_panel.get(), opacity);
}
    
void RenderMedia::forwardEvent(Event* event)
{
    if (event->isMouseEvent() && m_controlsShadowRoot) {
        MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
        IntPoint point(mouseEvent->pageX(), mouseEvent->pageY());
        if (m_muteButton && m_muteButton->renderer() && m_muteButton->renderer()->absoluteBoundingBoxRect().contains(point))
            m_muteButton->defaultEventHandler(event);
        if (m_playButton && m_playButton->renderer() && m_playButton->renderer()->absoluteBoundingBoxRect().contains(point))
            m_playButton->defaultEventHandler(event);
        if (m_seekBackButton && m_seekBackButton->renderer() && m_seekBackButton->renderer()->absoluteBoundingBoxRect().contains(point))
            m_seekBackButton->defaultEventHandler(event);
        if (m_seekForwardButton && m_seekForwardButton->renderer() && m_seekForwardButton->renderer()->absoluteBoundingBoxRect().contains(point))
            m_seekForwardButton->defaultEventHandler(event);
        if (m_timeline && m_timeline->renderer() && m_timeline->renderer()->absoluteBoundingBoxRect().contains(point))
            m_timeline->defaultEventHandler(event);
        if (m_fullscreenButton && m_fullscreenButton->renderer() && m_fullscreenButton->renderer()->absoluteBoundingBoxRect().contains(point))
            m_fullscreenButton->defaultEventHandler(event);
        
        if (event->type() == mouseoverEvent) {
            m_mouseOver = true;
            updateControlVisibility();
        }
        if (event->type() == mouseoutEvent) {
            // FIXME: moving over scrollbar thumb generates mouseout for the ancestor media element for some reason
            m_mouseOver = absoluteBoundingBoxRect().contains(point);
            updateControlVisibility();
        }
    }
}

} // namespace WebCore

#endif