1/*
2    Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
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#include "MediaPlayerPrivateQt.h"
22
23#include "Frame.h"
24#include "FrameView.h"
25#include "GraphicsContext.h"
26#include "GraphicsLayer.h"
27#include "HTMLMediaElement.h"
28#include "HTMLVideoElement.h"
29#include "Logging.h"
30#include "NetworkingContext.h"
31#include "NotImplemented.h"
32#include "RenderVideo.h"
33#include "TimeRanges.h"
34#include "Widget.h"
35
36#include <QMediaPlayerControl>
37#include <QMediaService>
38#include <QNetworkAccessManager>
39#include <QNetworkCookie>
40#include <QNetworkCookieJar>
41#include <QNetworkRequest>
42#include <QPainter>
43#include <QPoint>
44#include <QRect>
45#include <QTime>
46#include <QTimer>
47#include <QUrl>
48#include <limits>
49#include <qmediametadata.h>
50#include <qmultimedia.h>
51#include <wtf/HashSet.h>
52#include <wtf/text/CString.h>
53
54#if USE(ACCELERATED_COMPOSITING)
55#include "texmap/TextureMapper.h"
56#endif
57
58using namespace WTF;
59
60namespace WebCore {
61
62PassOwnPtr<MediaPlayerPrivateInterface> MediaPlayerPrivateQt::create(MediaPlayer* player)
63{
64    return adoptPtr(new MediaPlayerPrivateQt(player));
65}
66
67void MediaPlayerPrivateQt::registerMediaEngine(MediaEngineRegistrar registrar)
68{
69    registrar(create, getSupportedTypes, supportsType, 0, 0, 0);
70}
71
72void MediaPlayerPrivateQt::getSupportedTypes(HashSet<String> &supported)
73{
74    QStringList types = QMediaPlayer::supportedMimeTypes();
75
76    for (int i = 0; i < types.size(); i++) {
77        QString mime = types.at(i);
78        if (mime.startsWith(QString::fromLatin1("audio/")) || mime.startsWith(QString::fromLatin1("video/")))
79            supported.add(mime);
80    }
81}
82
83MediaPlayer::SupportsType MediaPlayerPrivateQt::supportsType(const String& mime, const String& codec, const KURL&)
84{
85    if (!mime.startsWith("audio/") && !mime.startsWith("video/"))
86        return MediaPlayer::IsNotSupported;
87
88    // Parse and trim codecs.
89    QString codecStr = codec;
90    QStringList codecList = codecStr.split(QLatin1Char(','), QString::SkipEmptyParts);
91    QStringList codecListTrimmed;
92    foreach (const QString& codecStrNotTrimmed, codecList) {
93        QString codecStrTrimmed = codecStrNotTrimmed.trimmed();
94        if (!codecStrTrimmed.isEmpty())
95            codecListTrimmed.append(codecStrTrimmed);
96    }
97
98    if (QMediaPlayer::hasSupport(mime, codecListTrimmed) >= QMultimedia::ProbablySupported)
99        return MediaPlayer::IsSupported;
100
101    return MediaPlayer::MayBeSupported;
102}
103
104MediaPlayerPrivateQt::MediaPlayerPrivateQt(MediaPlayer* player)
105    : m_webCorePlayer(player)
106    , m_mediaPlayer(new QMediaPlayer)
107    , m_mediaPlayerControl(0)
108    , m_networkState(MediaPlayer::Empty)
109    , m_readyState(MediaPlayer::HaveNothing)
110    , m_currentSize(0, 0)
111    , m_naturalSize(RenderVideo::defaultSize())
112    , m_isSeeking(false)
113    , m_composited(false)
114    , m_preload(MediaPlayer::Auto)
115    , m_bytesLoadedAtLastDidLoadingProgress(0)
116    , m_suppressNextPlaybackChanged(false)
117{
118    m_mediaPlayer->setVideoOutput(this);
119
120    // Signal Handlers
121    connect(m_mediaPlayer, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)),
122            this, SLOT(mediaStatusChanged(QMediaPlayer::MediaStatus)));
123    connect(m_mediaPlayer, SIGNAL(stateChanged(QMediaPlayer::State)),
124            this, SLOT(stateChanged(QMediaPlayer::State)));
125    connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)),
126            this, SLOT(handleError(QMediaPlayer::Error)));
127    connect(m_mediaPlayer, SIGNAL(bufferStatusChanged(int)),
128            this, SLOT(bufferStatusChanged(int)));
129    connect(m_mediaPlayer, SIGNAL(durationChanged(qint64)),
130            this, SLOT(durationChanged(qint64)));
131    connect(m_mediaPlayer, SIGNAL(positionChanged(qint64)),
132            this, SLOT(positionChanged(qint64)));
133    connect(m_mediaPlayer, SIGNAL(volumeChanged(int)),
134            this, SLOT(volumeChanged(int)));
135    connect(m_mediaPlayer, SIGNAL(mutedChanged(bool)),
136            this, SLOT(mutedChanged(bool)));
137    connect(this, SIGNAL(surfaceFormatChanged(const QVideoSurfaceFormat&)),
138            this, SLOT(surfaceFormatChanged(const QVideoSurfaceFormat&)));
139
140    // Grab the player control
141    if (QMediaService* service = m_mediaPlayer->service()) {
142        m_mediaPlayerControl = qobject_cast<QMediaPlayerControl *>(
143                service->requestControl(QMediaPlayerControl_iid));
144    }
145}
146
147MediaPlayerPrivateQt::~MediaPlayerPrivateQt()
148{
149    m_mediaPlayer->disconnect(this);
150    m_mediaPlayer->stop();
151    m_mediaPlayer->setMedia(QMediaContent());
152
153    delete m_mediaPlayer;
154}
155
156bool MediaPlayerPrivateQt::hasVideo() const
157{
158    return m_mediaPlayer->isVideoAvailable();
159}
160
161bool MediaPlayerPrivateQt::hasAudio() const
162{
163    return true;
164}
165
166void MediaPlayerPrivateQt::load(const String& url)
167{
168    m_mediaUrl = url;
169
170    // QtMultimedia does not have an API to throttle loading
171    // so we handle this ourselves by delaying the load
172    if (m_preload == MediaPlayer::None) {
173        m_delayingLoad = true;
174        return;
175    }
176
177    commitLoad(url);
178}
179
180void MediaPlayerPrivateQt::commitLoad(const String& url)
181{
182    // We are now loading
183    if (m_networkState != MediaPlayer::Loading) {
184        m_networkState = MediaPlayer::Loading;
185        m_webCorePlayer->networkStateChanged();
186    }
187
188    // And we don't have any data yet
189    if (m_readyState != MediaPlayer::HaveNothing) {
190        m_readyState = MediaPlayer::HaveNothing;
191        m_webCorePlayer->readyStateChanged();
192    }
193
194    KURL kUrl(ParsedURLString, url);
195    const QUrl rUrl = kUrl;
196    const QString scheme = rUrl.scheme().toLower();
197
198    // Grab the client media element
199    HTMLMediaElement* element = static_cast<HTMLMediaElement*>(m_webCorePlayer->mediaPlayerClient());
200
201    // Construct the media content with a network request if the resource is http[s]
202    if (scheme == QString::fromLatin1("http") || scheme == QString::fromLatin1("https")) {
203        QNetworkRequest request = QNetworkRequest(rUrl);
204
205        // Grab the current document
206        Document* document = element->document();
207        if (!document)
208            document = element->ownerDocument();
209
210        // Grab the frame and network manager
211        Frame* frame = document ? document->frame() : 0;
212        FrameLoader* frameLoader = frame ? frame->loader() : 0;
213        QNetworkAccessManager* manager = frameLoader ? frameLoader->networkingContext()->networkAccessManager() : 0;
214
215        if (manager) {
216            // Set the cookies
217            QNetworkCookieJar* jar = manager->cookieJar();
218            QList<QNetworkCookie> cookies = jar->cookiesForUrl(rUrl);
219
220            // Don't set the header if there are no cookies.
221            // This prevents a warning from being emitted.
222            if (!cookies.isEmpty())
223                request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
224
225            // Set the refferer, but not when requesting insecure content from a secure page
226            QUrl documentUrl = QUrl(QString(document->documentURI()));
227            if (documentUrl.scheme().toLower() == QString::fromLatin1("http") || scheme == QString::fromLatin1("https"))
228                request.setRawHeader("Referer", documentUrl.toEncoded());
229
230            // Set the user agent
231            request.setRawHeader("User-Agent", frameLoader->userAgent(rUrl).utf8().data());
232        }
233
234        m_mediaPlayer->setMedia(QMediaContent(request));
235    } else {
236        // Otherwise, just use the URL
237        m_mediaPlayer->setMedia(QMediaContent(rUrl));
238    }
239
240    // Set the current volume and mute status
241    // We get these from the element, rather than the player, in case we have
242    // transitioned from a media engine which doesn't support muting, to a media
243    // engine which does.
244    m_mediaPlayer->setMuted(element->muted());
245    m_mediaPlayer->setVolume(static_cast<int>(element->volume() * 100.0));
246
247    // Don't send PlaybackChanged notification for pre-roll.
248    m_suppressNextPlaybackChanged = true;
249
250    // Setting a media source will start loading the media, but we need
251    // to pre-roll as well to get video size-hints and buffer-status
252    if (element->paused())
253        m_mediaPlayer->pause();
254    else
255        m_mediaPlayer->play();
256}
257
258void MediaPlayerPrivateQt::resumeLoad()
259{
260    m_delayingLoad = false;
261
262    if (!m_mediaUrl.isNull())
263        commitLoad(m_mediaUrl);
264}
265
266void MediaPlayerPrivateQt::cancelLoad()
267{
268    m_mediaPlayer->setMedia(QMediaContent());
269    updateStates();
270}
271
272void MediaPlayerPrivateQt::prepareToPlay()
273{
274    if (m_mediaPlayer->media().isNull() || m_delayingLoad)
275        resumeLoad();
276}
277
278void MediaPlayerPrivateQt::play()
279{
280    if (m_mediaPlayer->state() != QMediaPlayer::PlayingState)
281        m_mediaPlayer->play();
282}
283
284void MediaPlayerPrivateQt::pause()
285{
286    if (m_mediaPlayer->state() == QMediaPlayer::PlayingState)
287        m_mediaPlayer->pause();
288}
289
290bool MediaPlayerPrivateQt::paused() const
291{
292    return (m_mediaPlayer->state() != QMediaPlayer::PlayingState);
293}
294
295void MediaPlayerPrivateQt::seek(float position)
296{
297    if (!m_mediaPlayer->isSeekable())
298        return;
299
300    if (m_mediaPlayerControl && !m_mediaPlayerControl->availablePlaybackRanges().contains(position * 1000))
301        return;
302
303    m_isSeeking = true;
304    m_mediaPlayer->setPosition(static_cast<qint64>(position * 1000));
305}
306
307bool MediaPlayerPrivateQt::seeking() const
308{
309    return m_isSeeking;
310}
311
312float MediaPlayerPrivateQt::duration() const
313{
314    if (m_readyState < MediaPlayer::HaveMetadata)
315        return 0.0f;
316
317    float duration = m_mediaPlayer->duration() / 1000.0f;
318
319    // We are streaming
320    if (duration <= 0.0f)
321        duration = std::numeric_limits<float>::infinity();
322
323    return duration;
324}
325
326float MediaPlayerPrivateQt::currentTime() const
327{
328    return m_mediaPlayer->position() / 1000.0f;
329}
330
331PassRefPtr<TimeRanges> MediaPlayerPrivateQt::buffered() const
332{
333    RefPtr<TimeRanges> buffered = TimeRanges::create();
334
335    if (!m_mediaPlayerControl)
336        return buffered;
337
338    QMediaTimeRange playbackRanges = m_mediaPlayerControl->availablePlaybackRanges();
339
340    foreach (const QMediaTimeInterval interval, playbackRanges.intervals()) {
341        float rangeMin = static_cast<float>(interval.start()) / 1000.0f;
342        float rangeMax = static_cast<float>(interval.end()) / 1000.0f;
343        buffered->add(rangeMin, rangeMax);
344    }
345
346    return buffered.release();
347}
348
349float MediaPlayerPrivateQt::maxTimeSeekable() const
350{
351    if (!m_mediaPlayerControl)
352        return 0;
353
354    return static_cast<float>(m_mediaPlayerControl->availablePlaybackRanges().latestTime()) / 1000.0f;
355}
356
357bool MediaPlayerPrivateQt::didLoadingProgress() const
358{
359    unsigned bytesLoaded = 0;
360    QLatin1String bytesLoadedKey("bytes-loaded");
361    if (m_mediaPlayer->availableMetaData().contains(bytesLoadedKey))
362        bytesLoaded = m_mediaPlayer->metaData(bytesLoadedKey).toInt();
363    else
364        bytesLoaded = m_mediaPlayer->bufferStatus();
365    bool didLoadingProgress = bytesLoaded != m_bytesLoadedAtLastDidLoadingProgress;
366    m_bytesLoadedAtLastDidLoadingProgress = bytesLoaded;
367    return didLoadingProgress;
368}
369
370unsigned MediaPlayerPrivateQt::totalBytes() const
371{
372    if (m_mediaPlayer->availableMetaData().contains(QMediaMetaData::Size))
373        return m_mediaPlayer->metaData(QMediaMetaData::Size).toInt();
374
375    return 100;
376}
377
378void MediaPlayerPrivateQt::setPreload(MediaPlayer::Preload preload)
379{
380    m_preload = preload;
381    if (m_delayingLoad && m_preload != MediaPlayer::None)
382        resumeLoad();
383}
384
385void MediaPlayerPrivateQt::setRate(float rate)
386{
387    m_mediaPlayer->setPlaybackRate(rate);
388}
389
390void MediaPlayerPrivateQt::setVolume(float volume)
391{
392    m_mediaPlayer->setVolume(static_cast<int>(volume * 100.0));
393}
394
395bool MediaPlayerPrivateQt::supportsMuting() const
396{
397    return true;
398}
399
400void MediaPlayerPrivateQt::setMuted(bool muted)
401{
402    m_mediaPlayer->setMuted(muted);
403}
404
405MediaPlayer::NetworkState MediaPlayerPrivateQt::networkState() const
406{
407    return m_networkState;
408}
409
410MediaPlayer::ReadyState MediaPlayerPrivateQt::readyState() const
411{
412    return m_readyState;
413}
414
415void MediaPlayerPrivateQt::setVisible(bool)
416{
417}
418
419void MediaPlayerPrivateQt::mediaStatusChanged(QMediaPlayer::MediaStatus)
420{
421    updateStates();
422}
423
424void MediaPlayerPrivateQt::handleError(QMediaPlayer::Error)
425{
426    updateStates();
427}
428
429void MediaPlayerPrivateQt::stateChanged(QMediaPlayer::State)
430{
431    if (!m_suppressNextPlaybackChanged)
432        m_webCorePlayer->playbackStateChanged();
433    else
434        m_suppressNextPlaybackChanged = false;
435}
436
437void MediaPlayerPrivateQt::surfaceFormatChanged(const QVideoSurfaceFormat& format)
438{
439    QSize size = format.sizeHint();
440    LOG(Media, "MediaPlayerPrivateQt::naturalSizeChanged(%dx%d)",
441            size.width(), size.height());
442
443    if (!size.isValid())
444        return;
445
446    IntSize webCoreSize = size;
447    if (webCoreSize == m_naturalSize)
448        return;
449
450    m_naturalSize = webCoreSize;
451    m_webCorePlayer->sizeChanged();
452}
453
454void MediaPlayerPrivateQt::positionChanged(qint64)
455{
456    // Only propagate this event if we are seeking
457    if (m_isSeeking) {
458        m_isSeeking = false;
459        m_webCorePlayer->timeChanged();
460    }
461}
462
463void MediaPlayerPrivateQt::bufferStatusChanged(int)
464{
465    notImplemented();
466}
467
468void MediaPlayerPrivateQt::durationChanged(qint64)
469{
470    m_webCorePlayer->durationChanged();
471}
472
473void MediaPlayerPrivateQt::volumeChanged(int volume)
474{
475    m_webCorePlayer->volumeChanged(static_cast<float>(volume) / 100.0);
476}
477
478void MediaPlayerPrivateQt::mutedChanged(bool muted)
479{
480    m_webCorePlayer->muteChanged(muted);
481}
482
483void MediaPlayerPrivateQt::updateStates()
484{
485    // Store the old states so that we can detect a change and raise change events
486    MediaPlayer::NetworkState oldNetworkState = m_networkState;
487    MediaPlayer::ReadyState oldReadyState = m_readyState;
488
489    QMediaPlayer::MediaStatus currentStatus = m_mediaPlayer->mediaStatus();
490    QMediaPlayer::Error currentError = m_mediaPlayer->error();
491
492    if (currentError != QMediaPlayer::NoError) {
493        m_readyState = MediaPlayer::HaveNothing;
494        if (currentError == QMediaPlayer::FormatError || currentError == QMediaPlayer::ResourceError)
495            m_networkState = MediaPlayer::FormatError;
496        else
497            m_networkState = MediaPlayer::NetworkError;
498    } else if (currentStatus == QMediaPlayer::UnknownMediaStatus
499               || currentStatus == QMediaPlayer::NoMedia) {
500        m_networkState = MediaPlayer::Idle;
501        m_readyState = MediaPlayer::HaveNothing;
502    } else if (currentStatus == QMediaPlayer::LoadingMedia) {
503        m_networkState = MediaPlayer::Loading;
504        m_readyState = MediaPlayer::HaveNothing;
505    } else if (currentStatus == QMediaPlayer::LoadedMedia) {
506        m_networkState = MediaPlayer::Loading;
507        m_readyState = MediaPlayer::HaveMetadata;
508    } else if (currentStatus == QMediaPlayer::BufferingMedia) {
509        m_networkState = MediaPlayer::Loading;
510        m_readyState = MediaPlayer::HaveFutureData;
511    } else if (currentStatus == QMediaPlayer::StalledMedia) {
512        m_networkState = MediaPlayer::Loading;
513        m_readyState = MediaPlayer::HaveCurrentData;
514    } else if (currentStatus == QMediaPlayer::BufferedMedia
515               || currentStatus == QMediaPlayer::EndOfMedia) {
516        m_networkState = MediaPlayer::Loaded;
517        m_readyState = MediaPlayer::HaveEnoughData;
518    } else if (currentStatus == QMediaPlayer::InvalidMedia) {
519        m_networkState = MediaPlayer::FormatError;
520        m_readyState = MediaPlayer::HaveNothing;
521    }
522
523    // Log the state changes and raise the state change events
524    // NB: The readyStateChanged event must come before the networkStateChanged event.
525    // Breaking this invariant will cause the resource selection algorithm for multiple
526    // sources to fail.
527    if (m_readyState != oldReadyState)
528        m_webCorePlayer->readyStateChanged();
529
530    if (m_networkState != oldNetworkState)
531        m_webCorePlayer->networkStateChanged();
532}
533
534void MediaPlayerPrivateQt::setSize(const IntSize& size)
535{
536    LOG(Media, "MediaPlayerPrivateQt::setSize(%dx%d)",
537            size.width(), size.height());
538
539    if (size == m_currentSize)
540        return;
541
542    m_currentSize = size;
543}
544
545IntSize MediaPlayerPrivateQt::naturalSize() const
546{
547    if (!hasVideo() ||  m_readyState < MediaPlayer::HaveMetadata) {
548        LOG(Media, "MediaPlayerPrivateQt::naturalSize() -> 0x0 (!hasVideo || !haveMetaData)");
549        return IntSize();
550    }
551
552    LOG(Media, "MediaPlayerPrivateQt::naturalSize() -> %dx%d (m_naturalSize)",
553            m_naturalSize.width(), m_naturalSize.height());
554
555    return m_naturalSize;
556}
557
558void MediaPlayerPrivateQt::removeVideoItem()
559{
560    m_mediaPlayer->setVideoOutput(static_cast<QAbstractVideoSurface*>(0));
561}
562
563void MediaPlayerPrivateQt::restoreVideoItem()
564{
565    m_mediaPlayer->setVideoOutput(this);
566}
567
568// Begin QAbstractVideoSurface implementation.
569
570bool MediaPlayerPrivateQt::start(const QVideoSurfaceFormat& format)
571{
572    m_currentVideoFrame = QVideoFrame();
573    m_frameFormat = format;
574
575    // If the pixel format is not supported by QImage, then we return false here and the QtMultimedia back-end
576    // will re-negotiate and call us again with a better format.
577    if (QVideoFrame::imageFormatFromPixelFormat(m_frameFormat.pixelFormat()) == QImage::Format_Invalid)
578        return false;
579
580    return QAbstractVideoSurface::start(format);
581}
582
583QList<QVideoFrame::PixelFormat> MediaPlayerPrivateQt::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const
584{
585    QList<QVideoFrame::PixelFormat> formats;
586    switch (handleType) {
587    case QAbstractVideoBuffer::QPixmapHandle:
588    case QAbstractVideoBuffer::NoHandle:
589        formats << QVideoFrame::Format_RGB32 << QVideoFrame::Format_ARGB32 << QVideoFrame::Format_RGB565;
590        break;
591    default: break;
592    }
593    return formats;
594}
595
596bool MediaPlayerPrivateQt::present(const QVideoFrame& frame)
597{
598    m_currentVideoFrame = frame;
599    m_webCorePlayer->repaint();
600    return true;
601}
602
603// End QAbstractVideoSurface implementation.
604
605void MediaPlayerPrivateQt::paint(GraphicsContext* context, const IntRect& rect)
606{
607#if USE(ACCELERATED_COMPOSITING)
608    if (m_composited)
609        return;
610#endif
611    paintCurrentFrameInContext(context, rect);
612}
613
614void MediaPlayerPrivateQt::paintCurrentFrameInContext(GraphicsContext* context, const IntRect& rect)
615{
616    if (context->paintingDisabled())
617        return;
618
619    if (!m_currentVideoFrame.isValid())
620        return;
621
622    QPainter* painter = context->platformContext();
623
624    if (m_currentVideoFrame.handleType() == QAbstractVideoBuffer::QPixmapHandle) {
625        painter->drawPixmap(rect, m_currentVideoFrame.handle().value<QPixmap>());
626    } else if (m_currentVideoFrame.map(QAbstractVideoBuffer::ReadOnly)) {
627        QImage image(m_currentVideoFrame.bits(),
628                     m_frameFormat.frameSize().width(),
629                     m_frameFormat.frameSize().height(),
630                     m_currentVideoFrame.bytesPerLine(),
631                     QVideoFrame::imageFormatFromPixelFormat(m_frameFormat.pixelFormat()));
632        const QRect target = rect;
633
634        if (m_frameFormat.scanLineDirection() == QVideoSurfaceFormat::BottomToTop) {
635            const QTransform oldTransform = painter->transform();
636            painter->scale(1, -1);
637            painter->translate(0, -target.bottom());
638            painter->drawImage(QRect(target.x(), 0, target.width(), target.height()), image);
639            painter->setTransform(oldTransform);
640        } else {
641            painter->drawImage(target, image);
642        }
643
644        m_currentVideoFrame.unmap();
645    }
646}
647
648#if USE(ACCELERATED_COMPOSITING)
649void MediaPlayerPrivateQt::paintToTextureMapper(TextureMapper* textureMapper, const FloatRect& targetRect, const TransformationMatrix& matrix, float opacity)
650{
651}
652#endif
653
654PlatformMedia MediaPlayerPrivateQt::platformMedia() const
655{
656    PlatformMedia pm;
657    pm.type = PlatformMedia::QtMediaPlayerType;
658    pm.media.qtMediaPlayer = const_cast<MediaPlayerPrivateQt*>(this);
659    return pm;
660}
661
662} // namespace WebCore
663
664#include "moc_MediaPlayerPrivateQt.cpp"
665