Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc. All rights reserved. | 2 * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc. All rights reserved. |
| 3 * | 3 * |
| 4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
| 5 * modification, are permitted provided that the following conditions | 5 * modification, are permitted provided that the following conditions |
| 6 * are met: | 6 * are met: |
| 7 * 1. Redistributions of source code must retain the above copyright | 7 * 1. Redistributions of source code must retain the above copyright |
| 8 * notice, this list of conditions and the following disclaimer. | 8 * notice, this list of conditions and the following disclaimer. |
| 9 * 2. Redistributions in binary form must reproduce the above copyright | 9 * 2. Redistributions in binary form must reproduce the above copyright |
| 10 * notice, this list of conditions and the following disclaimer in the | 10 * notice, this list of conditions and the following disclaimer in the |
| (...skipping 453 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 464 | 464 |
| 465 removeElementFromDocumentMap(this, &oldDocument); | 465 removeElementFromDocumentMap(this, &oldDocument); |
| 466 addElementToDocumentMap(this, &document()); | 466 addElementToDocumentMap(this, &document()); |
| 467 | 467 |
| 468 // FIXME: This is a temporary fix to prevent this object from causing the | 468 // FIXME: This is a temporary fix to prevent this object from causing the |
| 469 // MediaPlayer to dereference LocalFrame and FrameLoader pointers from the | 469 // MediaPlayer to dereference LocalFrame and FrameLoader pointers from the |
| 470 // previous document. This restarts the load, as if the src attribute had be en set. | 470 // previous document. This restarts the load, as if the src attribute had be en set. |
| 471 // A proper fix would provide a mechanism to allow this object to refresh | 471 // A proper fix would provide a mechanism to allow this object to refresh |
| 472 // the MediaPlayer's LocalFrame and FrameLoader references on | 472 // the MediaPlayer's LocalFrame and FrameLoader references on |
| 473 // document changes so that playback can be resumed properly. | 473 // document changes so that playback can be resumed properly. |
| 474 clearMediaPlayer(LoadMediaResource); | 474 invokeLoadAlgorith(); |
| 475 scheduleDelayedAction(LoadMediaResource); | 475 m_pendingActionFlags |= LoadMediaResource; |
| 476 if (!m_loadTimer.isActive()) | |
| 477 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); | |
| 476 | 478 |
| 477 // Decrement the load event delay count on oldDocument now that m_webMediaPl ayer has been destroyed | 479 // Decrement the load event delay count on oldDocument now that m_webMediaPl ayer has been destroyed |
| 478 // and there is no risk of dispatching a load event from within the destruct or. | 480 // and there is no risk of dispatching a load event from within the destruct or. |
| 479 oldDocument.decrementLoadEventDelayCount(); | 481 oldDocument.decrementLoadEventDelayCount(); |
| 480 | 482 |
| 481 ActiveDOMObject::didMoveToNewExecutionContext(&document()); | 483 ActiveDOMObject::didMoveToNewExecutionContext(&document()); |
| 482 HTMLElement::didMoveToNewDocument(oldDocument); | 484 HTMLElement::didMoveToNewDocument(oldDocument); |
| 483 } | 485 } |
| 484 | 486 |
| 485 bool HTMLMediaElement::supportsFocus() const | 487 bool HTMLMediaElement::supportsFocus() const |
| 486 { | 488 { |
| 487 if (ownerDocument()->isMediaDocument()) | 489 if (ownerDocument()->isMediaDocument()) |
| 488 return false; | 490 return false; |
| 489 | 491 |
| 490 // If no controls specified, we should still be able to focus the element if it has tabIndex. | 492 // If no controls specified, we should still be able to focus the element if it has tabIndex. |
| 491 return shouldShowControls() || HTMLElement::supportsFocus(); | 493 return shouldShowControls() || HTMLElement::supportsFocus(); |
| 492 } | 494 } |
| 493 | 495 |
| 494 bool HTMLMediaElement::isMouseFocusable() const | 496 bool HTMLMediaElement::isMouseFocusable() const |
| 495 { | 497 { |
| 496 return false; | 498 return false; |
| 497 } | 499 } |
| 498 | 500 |
| 499 void HTMLMediaElement::parseAttribute(const QualifiedName& name, const AtomicStr ing& oldValue, const AtomicString& value) | 501 void HTMLMediaElement::parseAttribute(const QualifiedName& name, const AtomicStr ing& oldValue, const AtomicString& value) |
| 500 { | 502 { |
| 501 if (name == srcAttr) { | 503 if (name == srcAttr) { |
| 502 // Trigger a reload, as long as the 'src' attribute is present. | 504 // Trigger a reload, as long as the 'src' attribute is present. |
| 503 if (!value.isNull()) { | 505 if (!value.isNull()) { |
| 504 clearMediaPlayer(LoadMediaResource); | 506 invokeLoadAlgorith(); |
| 505 scheduleDelayedAction(LoadMediaResource); | 507 m_pendingActionFlags |= LoadMediaResource; |
|
Srirama
2015/12/16 07:33:43
May be we can move this as well inside, probably w
philipj_slow
2015/12/16 15:51:07
Yes, unless all of this could be moved inside the
| |
| 508 if (!m_loadTimer.isActive()) | |
| 509 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); | |
| 506 } | 510 } |
| 507 } else if (name == controlsAttr) { | 511 } else if (name == controlsAttr) { |
| 508 configureMediaControls(); | 512 configureMediaControls(); |
| 509 } else if (name == preloadAttr) { | 513 } else if (name == preloadAttr) { |
| 510 setPlayerPreload(); | 514 setPlayerPreload(); |
| 511 } else { | 515 } else { |
| 512 HTMLElement::parseAttribute(name, oldValue, value); | 516 HTMLElement::parseAttribute(name, oldValue, value); |
| 513 } | 517 } |
| 514 } | 518 } |
| 515 | 519 |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 573 { | 577 { |
| 574 if (layoutObject()) | 578 if (layoutObject()) |
| 575 layoutObject()->updateFromElement(); | 579 layoutObject()->updateFromElement(); |
| 576 } | 580 } |
| 577 | 581 |
| 578 void HTMLMediaElement::scheduleDelayedAction(DelayedActionType actionType) | 582 void HTMLMediaElement::scheduleDelayedAction(DelayedActionType actionType) |
| 579 { | 583 { |
| 580 WTF_LOG(Media, "HTMLMediaElement::scheduleDelayedAction(%p)", this); | 584 WTF_LOG(Media, "HTMLMediaElement::scheduleDelayedAction(%p)", this); |
| 581 | 585 |
| 582 if ((actionType & LoadMediaResource) && !(m_pendingActionFlags & LoadMediaRe source)) { | 586 if ((actionType & LoadMediaResource) && !(m_pendingActionFlags & LoadMediaRe source)) { |
| 583 prepareForLoad(); | 587 invokeLoadAlgorith(); |
| 584 m_pendingActionFlags |= LoadMediaResource; | 588 m_pendingActionFlags |= LoadMediaResource; |
| 585 } | 589 } |
| 586 | 590 |
| 587 if (actionType & LoadTextTrackResource) | 591 if (actionType & LoadTextTrackResource) |
| 588 m_pendingActionFlags |= LoadTextTrackResource; | 592 m_pendingActionFlags |= LoadTextTrackResource; |
| 589 | 593 |
| 590 if (!m_loadTimer.isActive()) | 594 if (!m_loadTimer.isActive()) |
| 591 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); | 595 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); |
| 592 } | 596 } |
| 593 | 597 |
| 594 void HTMLMediaElement::scheduleNextSourceChild() | 598 void HTMLMediaElement::scheduleNextSourceChild() |
| 595 { | 599 { |
| 596 // Schedule the timer to try the next <source> element WITHOUT resetting sta te ala prepareForLoad. | 600 // Schedule the timer to try the next <source> element WITHOUT resetting sta te ala invokeLoadAlgorith. |
| 597 m_pendingActionFlags |= LoadMediaResource; | 601 m_pendingActionFlags |= LoadMediaResource; |
| 598 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); | 602 m_loadTimer.startOneShot(0, BLINK_FROM_HERE); |
| 599 } | 603 } |
| 600 | 604 |
| 601 void HTMLMediaElement::scheduleEvent(const AtomicString& eventName) | 605 void HTMLMediaElement::scheduleEvent(const AtomicString& eventName) |
| 602 { | 606 { |
| 603 scheduleEvent(Event::createCancelable(eventName)); | 607 scheduleEvent(Event::createCancelable(eventName)); |
| 604 } | 608 } |
| 605 | 609 |
| 606 void HTMLMediaElement::scheduleEvent(PassRefPtrWillBeRawPtr<Event> event) | 610 void HTMLMediaElement::scheduleEvent(PassRefPtrWillBeRawPtr<Event> event) |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 699 | 703 |
| 700 if (UserGestureIndicator::processingUserGesture() && m_userGestureRequiredFo rPlay) { | 704 if (UserGestureIndicator::processingUserGesture() && m_userGestureRequiredFo rPlay) { |
| 701 recordAutoplayMetric(AutoplayEnabledThroughLoad); | 705 recordAutoplayMetric(AutoplayEnabledThroughLoad); |
| 702 m_userGestureRequiredForPlay = false; | 706 m_userGestureRequiredForPlay = false; |
| 703 // While usergesture-initiated load()s technically count as autoplayed, | 707 // While usergesture-initiated load()s technically count as autoplayed, |
| 704 // they don't feel like such to the users and hence we don't want to | 708 // they don't feel like such to the users and hence we don't want to |
| 705 // count them for the purposes of metrics. | 709 // count them for the purposes of metrics. |
| 706 m_autoplayMediaCounted = true; | 710 m_autoplayMediaCounted = true; |
| 707 } | 711 } |
| 708 | 712 |
| 709 prepareForLoad(); | 713 invokeLoadAlgorith(); |
|
philipj_slow
2015/12/16 15:51:07
In fact, I would keep the name prepareForLoad() fo
| |
| 710 loadInternal(); | 714 loadInternal(); |
| 711 prepareToPlay(); | 715 prepareToPlay(); |
| 712 } | 716 } |
| 713 | 717 |
| 714 void HTMLMediaElement::prepareForLoad() | 718 // 4.8.13.5 Loading the media resource |
| 719 void HTMLMediaElement::invokeLoadAlgorith() | |
| 715 { | 720 { |
| 716 WTF_LOG(Media, "HTMLMediaElement::prepareForLoad(%p)", this); | 721 WTF_LOG(Media, "HTMLMediaElement::invokeLoadAlgorith(%p)", this); |
| 717 | 722 |
| 718 // Perform the cleanup required for the resource load algorithm to run. | 723 NetworkState networkState = m_networkState; |
| 719 stopPeriodicTimers(); | 724 resetMediaElement(LoadMediaResource); |
| 720 m_loadTimer.stop(); | |
| 721 cancelDeferredLoad(); | |
| 722 // FIXME: Figure out appropriate place to reset LoadTextTrackResource if nec essary and set m_pendingActionFlags to 0 here. | |
| 723 m_pendingActionFlags &= ~LoadMediaResource; | |
| 724 m_sentEndEvent = false; | |
| 725 m_sentStalledEvent = false; | |
| 726 m_haveFiredLoadedData = false; | |
| 727 m_completelyLoaded = false; | |
| 728 m_havePreparedToPlay = false; | |
| 729 m_displayMode = Unknown; | |
| 730 | 725 |
| 731 // 1 - Abort any already-running instance of the resource selection algorith m for this element. | 726 // 1 - Abort any already-running instance of the resource selection algorith m for this element. |
| 732 m_loadState = WaitingForSource; | 727 m_loadState = WaitingForSource; |
| 733 m_currentSourceNode = nullptr; | 728 m_currentSourceNode = nullptr; |
| 734 | 729 |
| 735 // 2 - If there are any tasks from the media element's media element event t ask source in | 730 // 2 - If there are any tasks from the media element's media element event t ask source in |
| 736 // one of the task queues, then remove those tasks. | 731 // one of the task queues, then remove those tasks. |
| 737 cancelPendingEventsAndCallbacks(); | 732 cancelPendingEventsAndCallbacks(); |
| 738 | 733 |
| 739 // 3 - If the media element's networkState is set to NETWORK_LOADING or NETW ORK_IDLE, queue | 734 // 3 - If the media element's networkState is set to NETWORK_LOADING or NETW ORK_IDLE, queue |
| 740 // a task to fire a simple event named abort at the media element. | 735 // a task to fire a simple event named abort at the media element. |
| 741 if (m_networkState == NETWORK_LOADING || m_networkState == NETWORK_IDLE) | 736 if (networkState == NETWORK_LOADING || networkState == NETWORK_IDLE) |
| 742 scheduleEvent(EventTypeNames::abort); | 737 scheduleEvent(EventTypeNames::abort); |
| 743 | 738 |
| 744 resetMediaPlayerAndMediaSource(); | |
| 745 | |
| 746 // 4 - If the media element's networkState is not set to NETWORK_EMPTY, then run these substeps | 739 // 4 - If the media element's networkState is not set to NETWORK_EMPTY, then run these substeps |
| 747 if (m_networkState != NETWORK_EMPTY) { | 740 if (networkState != NETWORK_EMPTY) { |
|
Srirama
2015/12/16 07:33:43
Most of this section is covered in resetMediaEleme
| |
| 748 // 4.1 - Queue a task to fire a simple event named emptied at the media element. | 741 // 4.1 - Queue a task to fire a simple event named emptied at the media element. |
| 749 scheduleEvent(EventTypeNames::emptied); | 742 scheduleEvent(EventTypeNames::emptied); |
| 750 | 743 |
| 751 // 4.2 - If a fetching process is in progress for the media element, the user agent should stop it. | 744 // 4.2 - If a fetching process is in progress for the media element, the user agent should stop it. |
| 752 setNetworkState(NETWORK_EMPTY); | 745 setNetworkState(NETWORK_EMPTY); |
| 753 | 746 |
| 754 // 4.3 - Forget the media element's media-resource-specific tracks. | 747 // 4.3 - Forget the media element's media-resource-specific tracks. |
| 755 forgetResourceSpecificTracks(); | 748 forgetResourceSpecificTracks(); |
| 756 | 749 |
| 757 // 4.4 - If readyState is not set to HAVE_NOTHING, then set it to that s tate. | 750 // 4.4 - If readyState is not set to HAVE_NOTHING, then set it to that s tate. |
| (...skipping 2221 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 2979 audioSourceProvider().setClient(nullptr); | 2972 audioSourceProvider().setClient(nullptr); |
| 2980 #endif | 2973 #endif |
| 2981 if (m_webMediaPlayer) { | 2974 if (m_webMediaPlayer) { |
| 2982 #if ENABLE(WEB_AUDIO) | 2975 #if ENABLE(WEB_AUDIO) |
| 2983 m_audioSourceProvider.wrap(nullptr); | 2976 m_audioSourceProvider.wrap(nullptr); |
| 2984 #endif | 2977 #endif |
| 2985 m_webMediaPlayer.clear(); | 2978 m_webMediaPlayer.clear(); |
| 2986 } | 2979 } |
| 2987 } | 2980 } |
| 2988 | 2981 |
| 2989 void HTMLMediaElement::clearMediaPlayer(int flags) | 2982 void HTMLMediaElement::resetMediaElement(int flags) |
| 2990 { | 2983 { |
| 2991 forgetResourceSpecificTracks(); | 2984 forgetResourceSpecificTracks(); |
| 2992 | 2985 |
| 2993 closeMediaSource(); | 2986 closeMediaSource(); |
| 2994 | 2987 |
| 2995 cancelDeferredLoad(); | 2988 cancelDeferredLoad(); |
| 2996 | 2989 |
| 2997 { | 2990 { |
| 2998 AudioSourceProviderClientLockScope scope(*this); | 2991 AudioSourceProviderClientLockScope scope(*this); |
| 2999 clearMediaPlayerAndAudioSourceProviderClientWithoutLocking(); | 2992 clearMediaPlayerAndAudioSourceProviderClientWithoutLocking(); |
| 3000 } | 2993 } |
| 3001 | 2994 |
| 2995 #if ENABLE(WEB_AUDIO) | |
| 2996 if (m_audioSourceNode) | |
| 2997 audioSourceProvider().setClient(m_audioSourceNode); | |
| 2998 #endif | |
| 2999 | |
| 3002 stopPeriodicTimers(); | 3000 stopPeriodicTimers(); |
| 3003 m_loadTimer.stop(); | 3001 m_loadTimer.stop(); |
| 3004 | 3002 |
| 3005 m_pendingActionFlags &= ~flags; | 3003 m_pendingActionFlags &= ~flags; |
| 3006 m_loadState = WaitingForSource; | 3004 m_loadState = WaitingForSource; |
| 3007 | 3005 |
| 3008 // We can't cast if we don't have a media player. | 3006 // We can't cast if we don't have a media player. |
| 3009 m_remoteRoutesAvailable = false; | 3007 m_remoteRoutesAvailable = false; |
| 3010 m_playingRemotely = false; | 3008 m_playingRemotely = false; |
| 3009 | |
| 3010 // FIXME: Figure out appropriate place to reset LoadTextTrackResource if nec essary and set m_pendingActionFlags to 0 here. | |
| 3011 m_sentEndEvent = false; | |
| 3012 m_sentStalledEvent = false; | |
| 3013 m_haveFiredLoadedData = false; | |
| 3014 m_completelyLoaded = false; | |
| 3015 m_havePreparedToPlay = false; | |
| 3016 m_displayMode = Unknown; | |
| 3017 | |
| 3018 m_readyState = HAVE_NOTHING; | |
| 3019 m_readyStateMaximum = HAVE_NOTHING; | |
| 3020 setNetworkState(NETWORK_EMPTY); | |
| 3021 setShouldDelayLoadEvent(false); | |
| 3022 m_currentSourceNode = nullptr; | |
| 3023 invalidateCachedTime(); | |
| 3024 cueTimeline().updateActiveCues(0); | |
| 3025 m_playing = false; | |
| 3026 m_paused = true; | |
| 3027 m_seeking = false; | |
| 3028 | |
| 3011 if (mediaControls()) | 3029 if (mediaControls()) |
| 3012 mediaControls()->refreshCastButtonVisibilityWithoutUpdate(); | 3030 mediaControls()->refreshCastButtonVisibilityWithoutUpdate(); |
| 3013 | 3031 |
| 3014 if (layoutObject()) | 3032 if (layoutObject()) |
| 3015 layoutObject()->setShouldDoFullPaintInvalidation(); | 3033 layoutObject()->setShouldDoFullPaintInvalidation(); |
| 3016 } | 3034 } |
| 3017 | 3035 |
| 3018 void HTMLMediaElement::stop() | 3036 void HTMLMediaElement::stop() |
| 3019 { | 3037 { |
| 3020 WTF_LOG(Media, "HTMLMediaElement::stop(%p)", this); | 3038 WTF_LOG(Media, "HTMLMediaElement::stop(%p)", this); |
| 3021 | 3039 |
| 3022 recordMetricsIfPausing(); | 3040 recordMetricsIfPausing(); |
| 3023 | 3041 |
| 3024 // Close the async event queue so that no events are enqueued. | 3042 // Close the async event queue so that no events are enqueued. |
| 3025 cancelPendingEventsAndCallbacks(); | 3043 cancelPendingEventsAndCallbacks(); |
| 3026 m_asyncEventQueue->close(); | 3044 m_asyncEventQueue->close(); |
| 3027 | 3045 |
| 3028 // Stop the playback without generating events | 3046 // Clear everything in the Media Element |
| 3029 clearMediaPlayer(-1); | 3047 resetMediaElement(-1); |
| 3030 m_readyState = HAVE_NOTHING; | |
| 3031 m_readyStateMaximum = HAVE_NOTHING; | |
| 3032 setNetworkState(NETWORK_EMPTY); | |
| 3033 setShouldDelayLoadEvent(false); | |
| 3034 m_currentSourceNode = nullptr; | |
| 3035 invalidateCachedTime(); | |
| 3036 cueTimeline().updateActiveCues(0); | |
| 3037 m_playing = false; | |
| 3038 m_paused = true; | |
| 3039 m_seeking = false; | |
| 3040 | 3048 |
| 3041 if (layoutObject()) | 3049 if (layoutObject()) |
| 3042 layoutObject()->updateFromElement(); | 3050 layoutObject()->updateFromElement(); |
| 3043 | 3051 |
| 3044 stopPeriodicTimers(); | |
| 3045 | |
| 3046 // Ensure that hasPendingActivity() is not preventing garbage collection, si nce otherwise this | 3052 // Ensure that hasPendingActivity() is not preventing garbage collection, si nce otherwise this |
| 3047 // media element will simply leak. | 3053 // media element will simply leak. |
| 3048 ASSERT(!hasPendingActivity()); | 3054 ASSERT(!hasPendingActivity()); |
| 3049 } | 3055 } |
| 3050 | 3056 |
| 3051 bool HTMLMediaElement::hasPendingActivity() const | 3057 bool HTMLMediaElement::hasPendingActivity() const |
| 3052 { | 3058 { |
| 3053 // The delaying-the-load-event flag is set by resource selection algorithm w hen looking for a | 3059 // The delaying-the-load-event flag is set by resource selection algorithm w hen looking for a |
| 3054 // resource to load, before networkState has reached to NETWORK_LOADING. | 3060 // resource to load, before networkState has reached to NETWORK_LOADING. |
| 3055 if (m_shouldDelayLoadEvent) | 3061 if (m_shouldDelayLoadEvent) |
| (...skipping 328 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3384 } | 3390 } |
| 3385 | 3391 |
| 3386 void* HTMLMediaElement::preDispatchEventHandler(Event* event) | 3392 void* HTMLMediaElement::preDispatchEventHandler(Event* event) |
| 3387 { | 3393 { |
| 3388 if (event && event->type() == EventTypeNames::webkitfullscreenchange) | 3394 if (event && event->type() == EventTypeNames::webkitfullscreenchange) |
| 3389 configureMediaControls(); | 3395 configureMediaControls(); |
| 3390 | 3396 |
| 3391 return nullptr; | 3397 return nullptr; |
| 3392 } | 3398 } |
| 3393 | 3399 |
| 3394 // TODO(srirama.m): Refactor this and clearMediaPlayer to the extent possible. | 3400 // TODO(srirama.m): Merge it to resetMediaElement if possible and remove it. |
| 3395 void HTMLMediaElement::resetMediaPlayerAndMediaSource() | 3401 void HTMLMediaElement::resetMediaPlayerAndMediaSource() |
| 3396 { | 3402 { |
| 3397 closeMediaSource(); | 3403 closeMediaSource(); |
| 3398 | 3404 |
| 3399 { | 3405 { |
| 3400 AudioSourceProviderClientLockScope scope(*this); | 3406 AudioSourceProviderClientLockScope scope(*this); |
| 3401 clearMediaPlayerAndAudioSourceProviderClientWithoutLocking(); | 3407 clearMediaPlayerAndAudioSourceProviderClientWithoutLocking(); |
| 3402 } | 3408 } |
| 3403 | 3409 |
| 3404 // We haven't yet found out if any remote routes are available. | 3410 // We haven't yet found out if any remote routes are available. |
| (...skipping 227 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3632 visitor->trace(m_client); | 3638 visitor->trace(m_client); |
| 3633 } | 3639 } |
| 3634 | 3640 |
| 3635 DEFINE_TRACE(HTMLMediaElement::AudioSourceProviderImpl) | 3641 DEFINE_TRACE(HTMLMediaElement::AudioSourceProviderImpl) |
| 3636 { | 3642 { |
| 3637 visitor->trace(m_client); | 3643 visitor->trace(m_client); |
| 3638 } | 3644 } |
| 3639 #endif | 3645 #endif |
| 3640 | 3646 |
| 3641 } | 3647 } |
| OLD | NEW |