1/*
2 * Copyright (C) 2007, 2008, 2009, 2010 Apple, Inc.  All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25#include "config.h"
26
27#include "QTMovieGWorld.h"
28
29#include "QTMovieTask.h"
30#include <GXMath.h>
31#include <Movies.h>
32#include <QTML.h>
33#include <QuickTimeComponents.h>
34#include <wtf/Assertions.h>
35#include <wtf/HashSet.h>
36#include <wtf/Noncopyable.h>
37#include <wtf/Vector.h>
38
39using namespace std;
40
41static const long minimumQuickTimeVersion = 0x07300000; // 7.3
42
43static LPCWSTR fullscreenQTMovieGWorldPointerProp = L"fullscreenQTMovieGWorldPointer";
44
45// Resizing GWorlds is slow, give them a minimum size so size of small
46// videos can be animated smoothly
47static const int cGWorldMinWidth = 640;
48static const int cGWorldMinHeight = 360;
49
50static const float cNonContinuousTimeChange = 0.2f;
51
52union UppParam {
53    long longValue;
54    void* ptr;
55};
56
57static MovieDrawingCompleteUPP gMovieDrawingCompleteUPP = 0;
58static HashSet<QTMovieGWorldPrivate*>* gTaskList;
59static Vector<CFStringRef>* gSupportedTypes = 0;
60static SInt32 quickTimeVersion = 0;
61
62class QTMovieGWorldPrivate : public QTMovieClient {
63public:
64    QTMovieGWorldPrivate(QTMovieGWorld* movieWin);
65    virtual ~QTMovieGWorldPrivate();
66
67    void registerDrawingCallback();
68    void unregisterDrawingCallback();
69    void drawingComplete();
70    void updateGWorld();
71    void createGWorld();
72    void deleteGWorld();
73    void clearGWorld();
74    void updateMovieSize();
75
76    void setSize(int, int);
77
78    virtual void movieEnded(QTMovie*);
79    virtual void movieLoadStateChanged(QTMovie*);
80    virtual void movieTimeChanged(QTMovie*);
81
82    QTMovieGWorld* m_movieWin;
83    RefPtr<QTMovie> m_qtMovie;
84    Movie m_movie;
85    QTMovieGWorldClient* m_client;
86    long m_loadState;
87    int m_width;
88    int m_height;
89    bool m_visible;
90    GWorldPtr m_gWorld;
91    int m_gWorldWidth;
92    int m_gWorldHeight;
93    GWorldPtr m_savedGWorld;
94    float m_widthScaleFactor;
95    float m_heightScaleFactor;
96#if !ASSERT_DISABLED
97    bool m_scaleCached;
98#endif
99    WindowPtr m_fullscreenWindow;
100    GWorldPtr m_fullscreenOrigGWorld;
101    Rect m_fullscreenRect;
102    QTMovieGWorldFullscreenClient* m_fullscreenClient;
103    char* m_fullscreenRestoreState;
104    bool m_disabled;
105};
106
107QTMovieGWorldPrivate::QTMovieGWorldPrivate(QTMovieGWorld* movieWin)
108    : m_movieWin(movieWin)
109    , m_movie(0)
110    , m_client(0)
111    , m_loadState(0)
112    , m_width(0)
113    , m_height(0)
114    , m_visible(false)
115    , m_gWorld(0)
116    , m_gWorldWidth(0)
117    , m_gWorldHeight(0)
118    , m_savedGWorld(0)
119    , m_widthScaleFactor(1)
120    , m_heightScaleFactor(1)
121#if !ASSERT_DISABLED
122    , m_scaleCached(false)
123#endif
124    , m_fullscreenWindow(0)
125    , m_fullscreenOrigGWorld(0)
126    , m_fullscreenClient(0)
127    , m_fullscreenRestoreState(0)
128    , m_disabled(false)
129    , m_qtMovie(0)
130{
131    Rect rect = { 0, 0, 0, 0 };
132    m_fullscreenRect = rect;
133}
134
135QTMovieGWorldPrivate::~QTMovieGWorldPrivate()
136{
137    ASSERT(!m_fullscreenWindow);
138
139    if (m_gWorld)
140        deleteGWorld();
141}
142
143pascal OSErr movieDrawingCompleteProc(Movie movie, long data)
144{
145    UppParam param;
146    param.longValue = data;
147    QTMovieGWorldPrivate* mp = static_cast<QTMovieGWorldPrivate*>(param.ptr);
148    if (mp)
149        mp->drawingComplete();
150    return 0;
151}
152
153void QTMovieGWorldPrivate::registerDrawingCallback()
154{
155    if (!gMovieDrawingCompleteUPP)
156        gMovieDrawingCompleteUPP = NewMovieDrawingCompleteUPP(movieDrawingCompleteProc);
157
158    UppParam param;
159    param.ptr = this;
160    SetMovieDrawingCompleteProc(m_movie, movieDrawingCallWhenChanged, gMovieDrawingCompleteUPP, param.longValue);
161}
162
163void QTMovieGWorldPrivate::unregisterDrawingCallback()
164{
165    SetMovieDrawingCompleteProc(m_movie, movieDrawingCallWhenChanged, 0, 0);
166}
167
168void QTMovieGWorldPrivate::drawingComplete()
169{
170    if (!m_gWorld || m_movieWin->m_private->m_disabled || m_loadState < QTMovieLoadStateLoaded)
171        return;
172    m_client->movieNewImageAvailable(m_movieWin);
173}
174
175void QTMovieGWorldPrivate::updateGWorld()
176{
177    bool shouldBeVisible = m_visible;
178    if (!m_height || !m_width)
179        shouldBeVisible = false;
180
181    if (shouldBeVisible && !m_gWorld)
182        createGWorld();
183    else if (!shouldBeVisible && m_gWorld)
184        deleteGWorld();
185    else if (m_gWorld && (m_width > m_gWorldWidth || m_height > m_gWorldHeight)) {
186        // need a bigger, better gWorld
187        deleteGWorld();
188        createGWorld();
189    }
190}
191
192void QTMovieGWorldPrivate::createGWorld()
193{
194    ASSERT(!m_gWorld);
195    if (!m_movie || m_loadState < QTMovieLoadStateLoaded)
196        return;
197
198    m_gWorldWidth = max(cGWorldMinWidth, m_width);
199    m_gWorldHeight = max(cGWorldMinHeight, m_height);
200    Rect bounds;
201    bounds.top = 0;
202    bounds.left = 0;
203    bounds.right = m_gWorldWidth;
204    bounds.bottom = m_gWorldHeight;
205    OSErr err = QTNewGWorld(&m_gWorld, k32BGRAPixelFormat, &bounds, 0, 0, 0);
206    if (err)
207        return;
208    GetMovieGWorld(m_movie, &m_savedGWorld, 0);
209    SetMovieGWorld(m_movie, m_gWorld, 0);
210    bounds.right = m_width;
211    bounds.bottom = m_height;
212    SetMovieBox(m_movie, &bounds);
213}
214
215void QTMovieGWorldPrivate::clearGWorld()
216{
217    if (!m_movie || !m_gWorld)
218        return;
219
220    GrafPtr savePort;
221    GetPort(&savePort);
222    MacSetPort((GrafPtr)m_gWorld);
223
224    Rect bounds;
225    bounds.top = 0;
226    bounds.left = 0;
227    bounds.right = m_gWorldWidth;
228    bounds.bottom = m_gWorldHeight;
229    EraseRect(&bounds);
230
231    MacSetPort(savePort);
232}
233
234void QTMovieGWorldPrivate::setSize(int width, int height)
235{
236    if (m_width == width && m_height == height)
237        return;
238    m_width = width;
239    m_height = height;
240
241    // Do not change movie box before reaching load state loaded as we grab
242    // the initial size when task() sees that state for the first time, and
243    // we need the initial size to be able to scale movie properly.
244    if (!m_movie || m_loadState < QTMovieLoadStateLoaded)
245        return;
246
247#if !ASSERT_DISABLED
248    ASSERT(m_scaleCached);
249#endif
250
251    updateMovieSize();
252}
253
254void QTMovieGWorldPrivate::updateMovieSize()
255{
256    if (!m_movie || m_loadState < QTMovieLoadStateLoaded)
257        return;
258
259    Rect bounds;
260    bounds.top = 0;
261    bounds.left = 0;
262    bounds.right = m_width;
263    bounds.bottom = m_height;
264    SetMovieBox(m_movie, &bounds);
265    updateGWorld();
266}
267
268
269void QTMovieGWorldPrivate::deleteGWorld()
270{
271    ASSERT(m_gWorld);
272    if (m_movie)
273        SetMovieGWorld(m_movie, m_savedGWorld, 0);
274    m_savedGWorld = 0;
275    DisposeGWorld(m_gWorld);
276    m_gWorld = 0;
277    m_gWorldWidth = 0;
278    m_gWorldHeight = 0;
279}
280
281void QTMovieGWorldPrivate::movieEnded(QTMovie*)
282{
283    // Do nothing
284}
285
286void QTMovieGWorldPrivate::movieLoadStateChanged(QTMovie* movie)
287{
288    long loadState = GetMovieLoadState(movie->getMovieHandle());
289    if (loadState != m_loadState) {
290
291        // we only need to erase the movie gworld when the load state changes to loaded while it
292        //  is visible as the gworld is destroyed/created when visibility changes
293        bool movieNewlyPlayable = loadState >= QTMovieLoadStateLoaded && m_loadState < QTMovieLoadStateLoaded;
294        m_loadState = loadState;
295
296        if (movieNewlyPlayable) {
297            updateMovieSize();
298            if (m_visible)
299                clearGWorld();
300        }
301    }
302}
303
304void QTMovieGWorldPrivate::movieTimeChanged(QTMovie*)
305{
306    // Do nothing
307}
308
309QTMovieGWorld::QTMovieGWorld(QTMovieGWorldClient* client)
310    : m_private(new QTMovieGWorldPrivate(this))
311{
312    m_private->m_client = client;
313}
314
315QTMovieGWorld::~QTMovieGWorld()
316{
317    delete m_private;
318}
319
320void QTMovieGWorld::setSize(int width, int height)
321{
322    m_private->setSize(width, height);
323    QTMovieTask::sharedTask()->updateTaskTimer();
324}
325
326void QTMovieGWorld::setVisible(bool b)
327{
328    m_private->m_visible = b;
329    m_private->updateGWorld();
330}
331
332void QTMovieGWorld::getCurrentFrameInfo(void*& buffer, unsigned& bitsPerPixel, unsigned& rowBytes, unsigned& width, unsigned& height)
333{
334    if (!m_private->m_gWorld) {
335        buffer = 0;
336        bitsPerPixel = 0;
337        rowBytes = 0;
338        width = 0;
339        height = 0;
340        return;
341    }
342    PixMapHandle offscreenPixMap = GetGWorldPixMap(m_private->m_gWorld);
343    buffer = (*offscreenPixMap)->baseAddr;
344    bitsPerPixel = (*offscreenPixMap)->pixelSize;
345    rowBytes = (*offscreenPixMap)->rowBytes & 0x3FFF;
346    width = m_private->m_width;
347    height = m_private->m_height;
348}
349
350void QTMovieGWorld::paint(HDC hdc, int x, int y)
351{
352    if (!m_private->m_gWorld)
353        return;
354
355    HDC hdcSrc = static_cast<HDC>(GetPortHDC(reinterpret_cast<GrafPtr>(m_private->m_gWorld)));
356    if (!hdcSrc)
357        return;
358
359    // FIXME: If we could determine the movie has no alpha, we could use BitBlt for those cases, which might be faster.
360    BLENDFUNCTION blendFunction;
361    blendFunction.BlendOp = AC_SRC_OVER;
362    blendFunction.BlendFlags = 0;
363    blendFunction.SourceConstantAlpha = 255;
364    blendFunction.AlphaFormat = AC_SRC_ALPHA;
365    AlphaBlend(hdc, x, y, m_private->m_width, m_private->m_height, hdcSrc,
366         0, 0, m_private->m_width, m_private->m_height, blendFunction);
367}
368
369void QTMovieGWorld::setDisabled(bool b)
370{
371    m_private->m_disabled = b;
372}
373
374bool QTMovieGWorld::isDisabled() const
375{
376    return m_private->m_disabled;
377}
378
379LRESULT QTMovieGWorld::fullscreenWndProc(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam)
380{
381    QTMovieGWorld* movie = static_cast<QTMovieGWorld*>(GetPropW(wnd, fullscreenQTMovieGWorldPointerProp));
382
383    if (message == WM_DESTROY)
384        RemovePropW(wnd, fullscreenQTMovieGWorldPointerProp);
385
386    if (!movie)
387        return DefWindowProc(wnd, message, wParam, lParam);
388
389    return movie->m_private->m_fullscreenClient->fullscreenClientWndProc(wnd, message, wParam, lParam);
390}
391
392HWND QTMovieGWorld::enterFullscreen(QTMovieGWorldFullscreenClient* client)
393{
394    m_private->m_fullscreenClient = client;
395
396    BeginFullScreen(&m_private->m_fullscreenRestoreState, 0, 0, 0, &m_private->m_fullscreenWindow, 0, fullScreenAllowEvents);
397    QTMLSetWindowWndProc(m_private->m_fullscreenWindow, fullscreenWndProc);
398    CreatePortAssociation(GetPortNativeWindow(m_private->m_fullscreenWindow), 0, 0);
399
400    GetMovieBox(m_private->m_movie, &m_private->m_fullscreenRect);
401    GetMovieGWorld(m_private->m_movie, &m_private->m_fullscreenOrigGWorld, 0);
402    SetMovieGWorld(m_private->m_movie, reinterpret_cast<CGrafPtr>(m_private->m_fullscreenWindow), GetGWorldDevice(reinterpret_cast<CGrafPtr>(m_private->m_fullscreenWindow)));
403
404    // Set the size of the box to preserve aspect ratio
405    Rect rect = m_private->m_fullscreenWindow->portRect;
406
407    float movieRatio = static_cast<float>(m_private->m_width) / m_private->m_height;
408    int windowWidth =  rect.right - rect.left;
409    int windowHeight = rect.bottom - rect.top;
410    float windowRatio = static_cast<float>(windowWidth) / windowHeight;
411    int actualWidth = (windowRatio > movieRatio) ? (windowHeight * movieRatio) : windowWidth;
412    int actualHeight = (windowRatio < movieRatio) ? (windowWidth / movieRatio) : windowHeight;
413    int offsetX = (windowWidth - actualWidth) / 2;
414    int offsetY = (windowHeight - actualHeight) / 2;
415
416    rect.left = offsetX;
417    rect.right = offsetX + actualWidth;
418    rect.top = offsetY;
419    rect.bottom = offsetY + actualHeight;
420
421    SetMovieBox(m_private->m_movie, &rect);
422    ShowHideTaskBar(true);
423
424    // Set the 'this' pointer on the HWND
425    HWND wnd = static_cast<HWND>(GetPortNativeWindow(m_private->m_fullscreenWindow));
426    SetPropW(wnd, fullscreenQTMovieGWorldPointerProp, static_cast<HANDLE>(this));
427
428    return wnd;
429}
430
431void QTMovieGWorld::exitFullscreen()
432{
433    if (!m_private->m_fullscreenWindow)
434        return;
435
436    HWND wnd = static_cast<HWND>(GetPortNativeWindow(m_private->m_fullscreenWindow));
437    DestroyPortAssociation(reinterpret_cast<CGrafPtr>(m_private->m_fullscreenWindow));
438    SetMovieGWorld(m_private->m_movie, m_private->m_fullscreenOrigGWorld, 0);
439    EndFullScreen(m_private->m_fullscreenRestoreState, 0L);
440    SetMovieBox(m_private->m_movie, &m_private->m_fullscreenRect);
441    m_private->m_fullscreenWindow = 0;
442}
443
444void QTMovieGWorld::setMovie(PassRefPtr<QTMovie> movie)
445{
446    if (m_private->m_qtMovie) {
447        m_private->unregisterDrawingCallback();
448        m_private->m_qtMovie->removeClient(m_private);
449        m_private->m_qtMovie = 0;
450        m_private->m_movie = 0;
451    }
452
453    m_private->m_qtMovie = movie;
454
455    if (m_private->m_qtMovie) {
456        m_private->m_qtMovie->addClient(m_private);
457        m_private->m_movie = m_private->m_qtMovie->getMovieHandle();
458        m_private->registerDrawingCallback();
459    }
460}
461
462QTMovie* QTMovieGWorld::movie() const
463{
464    return m_private->m_qtMovie.get();
465}
466