| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/html/media/html_media_element.h" |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/platform/web_fullscreen_video_status.h" |
| #include "third_party/blink/renderer/core/dom/events/native_event_listener.h" |
| #include "third_party/blink/renderer/core/event_type_names.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/fullscreen/fullscreen.h" |
| #include "third_party/blink/renderer/core/html/media/html_video_element.h" |
| #include "third_party/blink/renderer/core/html/media/media_controls.h" |
| #include "third_party/blink/renderer/core/html/media/media_custom_controls_fullscreen_detector.h" |
| #include "third_party/blink/renderer/core/html/track/text_track.h" |
| #include "third_party/blink/renderer/core/html/track/text_track_cue_list.h" |
| #include "third_party/blink/renderer/core/html/track/vtt/vtt_cue.h" |
| #include "third_party/blink/renderer/core/loader/empty_clients.h" |
| #include "third_party/blink/renderer/core/testing/page_test_base.h" |
| #include "third_party/blink/renderer/platform/bindings/microtask.h" |
| #include "third_party/blink/renderer/platform/testing/empty_web_media_player.h" |
| #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/testing_platform_support.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Most methods are faked rather than mocked. Faking avoids naggy warnings |
| // about unexpected calls. HTMLMediaElement <-> WebMediaplayer interface is |
| // highly complex and not generally the focus these tests (with the |
| // exception of the mocked methods). |
| class FakeWebMediaPlayer final : public EmptyWebMediaPlayer { |
| public: |
| FakeWebMediaPlayer(WebMediaPlayerClient* client, |
| ExecutionContext* context, |
| double duration) |
| : client_(client), context_(context), duration_(duration) {} |
| |
| MOCK_METHOD1(SetIsEffectivelyFullscreen, |
| void(blink::WebFullscreenVideoStatus)); |
| |
| double CurrentTime() const override { |
| return current_time_; |
| } |
| |
| // Establish a large so tests can attempt seeking. |
| double Duration() const override { return duration_; } |
| |
| WebTimeRanges Seekable() const override { |
| WebTimeRange single_range[] = {WebTimeRange(0, Duration())}; |
| |
| return WebTimeRanges(single_range, 1); |
| } |
| |
| void Seek(double seconds) override { last_seek_time_ = seconds; } |
| |
| void Play() override { |
| playing_ = true; |
| ScheduleTimeIncrement(); |
| } |
| void Pause() override { playing_ = false; } |
| bool Paused() const override { return !playing_; } |
| bool IsEnded() const override { return current_time_ == duration_; } |
| |
| void FinishSeek() { |
| ASSERT_GE(last_seek_time_, 0); |
| current_time_ = last_seek_time_; |
| last_seek_time_ = -1; |
| |
| client_->TimeChanged(); |
| if (playing_) |
| ScheduleTimeIncrement(); |
| } |
| |
| void SetAutoIncrementTimeDelta(base::Optional<base::TimeDelta> delta) { |
| auto_time_increment_delta_ = delta; |
| ScheduleTimeIncrement(); |
| } |
| |
| private: |
| void ScheduleTimeIncrement() { |
| if (scheduled_time_increment_) { |
| return; |
| } |
| if (!auto_time_increment_delta_.has_value()) { |
| return; |
| } |
| |
| context_->GetTaskRunner(TaskType::kInternalMediaRealTime) |
| ->PostDelayedTask(FROM_HERE, |
| base::BindOnce(&FakeWebMediaPlayer::AutoTimeIncrement, |
| base::Unretained(this), |
| auto_time_increment_delta_.value()), |
| auto_time_increment_delta_.value()); |
| scheduled_time_increment_ = true; |
| } |
| |
| void AutoTimeIncrement(base::TimeDelta time_delta) { |
| // If time increments have been disabled since posting the task, bail out |
| if (!auto_time_increment_delta_.has_value() || !playing_) { |
| return; |
| } |
| |
| scheduled_time_increment_ = false; |
| current_time_ += time_delta.InSecondsF(); |
| |
| // Notify the client if we've reached the end of the set duration |
| if (current_time_ >= duration_) { |
| current_time_ = duration_; |
| client_->TimeChanged(); |
| } else { |
| ScheduleTimeIncrement(); |
| } |
| |
| // Run V8 Microtasks (update OfficialPlaybackPosition) |
| Microtask::PerformCheckpoint(context_->GetIsolate()); |
| } |
| |
| WebMediaPlayerClient* client_; |
| WeakPersistent<ExecutionContext> context_; |
| mutable double current_time_ = 0; |
| bool playing_ = false; |
| base::Optional<base::TimeDelta> auto_time_increment_delta_ = |
| base::TimeDelta::FromMilliseconds(33); |
| bool scheduled_time_increment_ = false; |
| double last_seek_time_ = -1; |
| const double duration_; |
| }; |
| |
| class MediaStubLocalFrameClient : public EmptyLocalFrameClient { |
| public: |
| std::unique_ptr<WebMediaPlayer> CreateWebMediaPlayer( |
| HTMLMediaElement& element, |
| const WebMediaPlayerSource&, |
| WebMediaPlayerClient* client) override { |
| return std::make_unique<FakeWebMediaPlayer>( |
| client, element.GetExecutionContext(), media_duration_); |
| } |
| |
| void SetMediaDuration(double media_duration) { |
| media_duration_ = media_duration; |
| } |
| |
| private: |
| double media_duration_ = 1000000; |
| }; |
| |
| using testing::_; |
| using testing::AtLeast; |
| using testing::Return; |
| |
| } // anonymous namespace |
| |
| class HTMLMediaElementEventListenersTest : public PageTestBase { |
| protected: |
| void SetUp() override { |
| SetupPageWithClients(nullptr, |
| MakeGarbageCollected<MediaStubLocalFrameClient>()); |
| } |
| |
| void DestroyDocument() { PageTestBase::TearDown(); } |
| |
| HTMLVideoElement* Video() { |
| return To<HTMLVideoElement>(GetDocument().QuerySelector("video")); |
| } |
| |
| FakeWebMediaPlayer* WebMediaPlayer() { |
| return static_cast<FakeWebMediaPlayer*>(Video()->GetWebMediaPlayer()); |
| } |
| |
| MediaStubLocalFrameClient* LocalFrameClient() { |
| return static_cast<MediaStubLocalFrameClient*>(GetFrame().Client()); |
| } |
| |
| void SetMediaDuration(double duration) { |
| LocalFrameClient()->SetMediaDuration(duration); |
| } |
| |
| MediaControls* Controls() { return Video()->GetMediaControls(); } |
| |
| void SimulateReadyState(HTMLMediaElement::ReadyState state) { |
| Video()->SetReadyState(state); |
| } |
| |
| void SimulateNetworkState(HTMLMediaElement::NetworkState state) { |
| Video()->SetNetworkState(state); |
| } |
| |
| MediaCustomControlsFullscreenDetector* FullscreenDetector() { |
| return Video()->custom_controls_fullscreen_detector_; |
| } |
| }; |
| |
| TEST_F(HTMLMediaElementEventListenersTest, RemovingFromDocumentCollectsAll) { |
| EXPECT_EQ(Video(), nullptr); |
| GetDocument().body()->setInnerHTML("<video controls></video>"); |
| EXPECT_NE(Video(), nullptr); |
| EXPECT_TRUE(Video()->HasEventListeners()); |
| EXPECT_NE(Controls(), nullptr); |
| EXPECT_TRUE(GetDocument().HasEventListeners()); |
| |
| WeakPersistent<HTMLVideoElement> weak_persistent_video = Video(); |
| WeakPersistent<MediaControls> weak_persistent_controls = Controls(); |
| { |
| Persistent<HTMLVideoElement> persistent_video = Video(); |
| GetDocument().body()->setInnerHTML(""); |
| |
| // When removed from the document, the event listeners should have been |
| // dropped. |
| EXPECT_FALSE(GetDocument().HasEventListeners()); |
| // The video element should still have some event listeners. |
| EXPECT_TRUE(persistent_video->HasEventListeners()); |
| } |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| // They have been GC'd. |
| EXPECT_EQ(weak_persistent_video, nullptr); |
| EXPECT_EQ(weak_persistent_controls, nullptr); |
| } |
| |
| TEST_F(HTMLMediaElementEventListenersTest, |
| ReInsertingInDocumentCollectsControls) { |
| EXPECT_EQ(Video(), nullptr); |
| GetDocument().body()->setInnerHTML("<video controls></video>"); |
| EXPECT_NE(Video(), nullptr); |
| EXPECT_TRUE(Video()->HasEventListeners()); |
| EXPECT_NE(Controls(), nullptr); |
| EXPECT_TRUE(GetDocument().HasEventListeners()); |
| |
| // This should be a no-op. We keep a reference on the VideoElement to avoid an |
| // unexpected GC. |
| { |
| Persistent<HTMLVideoElement> video_holder = Video(); |
| GetDocument().body()->RemoveChild(Video()); |
| GetDocument().body()->AppendChild(video_holder.Get()); |
| } |
| |
| EXPECT_TRUE(GetDocument().HasEventListeners()); |
| EXPECT_TRUE(Video()->HasEventListeners()); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_NE(Video(), nullptr); |
| EXPECT_NE(Controls(), nullptr); |
| EXPECT_EQ(Controls(), Video()->GetMediaControls()); |
| } |
| |
| TEST_F(HTMLMediaElementEventListenersTest, |
| FullscreenDetectorTimerCancelledOnContextDestroy) { |
| EXPECT_EQ(Video(), nullptr); |
| GetDocument().body()->setInnerHTML("<video></video>"); |
| Video()->SetSrc("http://example.com"); |
| |
| test::RunPendingTasks(); |
| |
| EXPECT_NE(WebMediaPlayer(), nullptr); |
| |
| // Set ReadyState as HaveMetadata and go fullscreen, so the timer is fired. |
| EXPECT_NE(Video(), nullptr); |
| SimulateReadyState(HTMLMediaElement::kHaveMetadata); |
| LocalFrame::NotifyUserActivation( |
| GetDocument().GetFrame(), mojom::UserActivationNotificationType::kTest); |
| Fullscreen::RequestFullscreen(*Video()); |
| Fullscreen::DidResolveEnterFullscreenRequest(GetDocument(), |
| true /* granted */); |
| |
| test::RunPendingTasks(); |
| |
| Persistent<Document> persistent_document = &GetDocument(); |
| Persistent<MediaCustomControlsFullscreenDetector> detector = |
| FullscreenDetector(); |
| |
| Vector<blink::WebFullscreenVideoStatus> observed_results; |
| |
| ON_CALL(*WebMediaPlayer(), SetIsEffectivelyFullscreen(_)) |
| .WillByDefault(testing::Invoke( |
| [&](blink::WebFullscreenVideoStatus fullscreen_video_status) { |
| observed_results.push_back(fullscreen_video_status); |
| })); |
| |
| DestroyDocument(); |
| |
| test::RunPendingTasks(); |
| |
| // Document should not have listeners as the ExecutionContext is destroyed. |
| EXPECT_FALSE(persistent_document->HasEventListeners()); |
| // Should only notify the kNotEffectivelyFullscreen value when |
| // ExecutionContext is destroyed. |
| EXPECT_EQ(1u, observed_results.size()); |
| EXPECT_EQ(blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen, |
| observed_results[0]); |
| } |
| |
| class MockEventListener final : public NativeEventListener { |
| public: |
| MOCK_METHOD2(Invoke, void(ExecutionContext* executionContext, Event*)); |
| }; |
| |
| class HTMLMediaElementWithMockSchedulerTest |
| : public HTMLMediaElementEventListenersTest { |
| protected: |
| void SetUp() override { |
| EnablePlatform(); |
| // We want total control over when to advance the clock. This also allows |
| // us to call platform()->RunUntilIdle() to run all pending tasks without |
| // fear of looping forever. |
| platform()->SetAutoAdvanceNowToPendingTasks(false); |
| |
| // DocumentParserTiming has DCHECKS to make sure time > 0.0. |
| platform()->AdvanceClockSeconds(1); |
| |
| HTMLMediaElementEventListenersTest::SetUp(); |
| } |
| }; |
| |
| TEST_F(HTMLMediaElementWithMockSchedulerTest, OneTimeupdatePerSeek) { |
| testing::InSequence dummy; |
| GetDocument().body()->setInnerHTML("<video></video>"); |
| |
| // Set a src to trigger WebMediaPlayer creation. |
| Video()->SetSrc("http://example.com"); |
| |
| platform()->RunUntilIdle(); |
| ASSERT_NE(WebMediaPlayer(), nullptr); |
| |
| auto* timeupdate_handler = MakeGarbageCollected<MockEventListener>(); |
| Video()->addEventListener(event_type_names::kTimeupdate, timeupdate_handler); |
| |
| // Simulate conditions where playback is possible. |
| SimulateNetworkState(HTMLMediaElement::kNetworkIdle); |
| SimulateReadyState(HTMLMediaElement::kHaveFutureData); |
| |
| // Simulate advancing playback time. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta( |
| base::TimeDelta::FromMilliseconds(33)); |
| Video()->Play(); |
| |
| // While playing, timeupdate should fire every 250 ms -> 4x per second as long |
| // as media player's CurrentTime continues to advance. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(4); |
| platform()->RunForPeriodSeconds(1); |
| |
| // If media playback time is fixed, periodic timeupdate's should not continue |
| // to fire. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta(base::nullopt); |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0); |
| platform()->RunForPeriodSeconds(1); |
| |
| // Per spec, pausing should fire `timeupdate` |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| Video()->pause(); |
| platform()->RunUntilIdle(); |
| |
| // Seek to some time in the past. A completed seek while paused should trigger |
| // a *single* timeupdate. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| |
| // The WebMediaPlayer current time should have progressed to almost 1 second |
| // (Actually 0.99 due to |kFakeMediaPlayerAutoIncrementTimeDelta|). |
| ASSERT_GE(WebMediaPlayer()->CurrentTime(), 0.95); |
| Video()->setCurrentTime(0.5); |
| |
| // Fake the callback from WebMediaPlayer to complete the seek. |
| WebMediaPlayer()->FinishSeek(); |
| |
| // Give the scheduled timeupdate a chance to fire. |
| platform()->RunUntilIdle(); |
| } |
| |
| TEST_F(HTMLMediaElementWithMockSchedulerTest, PeriodicTimeupdateAfterSeek) { |
| testing::InSequence dummy; |
| GetDocument().body()->setInnerHTML("<video></video>"); |
| |
| // Set a src to trigger WebMediaPlayer creation. |
| Video()->SetSrc("http://example.com"); |
| |
| platform()->RunUntilIdle(); |
| EXPECT_NE(WebMediaPlayer(), nullptr); |
| |
| auto* timeupdate_handler = MakeGarbageCollected<MockEventListener>(); |
| Video()->addEventListener(event_type_names::kTimeupdate, timeupdate_handler); |
| |
| // Simulate conditions where playback is possible. |
| SimulateNetworkState(HTMLMediaElement::kNetworkIdle); |
| SimulateReadyState(HTMLMediaElement::kHaveFutureData); |
| |
| // Simulate advancing playback time to enable periodic timeupdates. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta( |
| base::TimeDelta::FromMilliseconds(8)); |
| Video()->Play(); |
| |
| // Advance a full periodic timeupdate interval (250 ms) and expect a single |
| // timeupdate. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| platform()->RunForPeriodSeconds(.250); |
| // The event is scheduled, but needs one more scheduler cycle to fire. |
| platform()->RunUntilIdle(); |
| |
| // Now advance 125 ms to reach the middle of the periodic timeupdate interval. |
| // no additional timeupdate should trigger. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0); |
| platform()->RunForPeriodSeconds(.125); |
| platform()->RunUntilIdle(); |
| |
| // While still in the middle of the periodic timeupdate interval, start and |
| // complete a seek and verify that a *non-periodic* timeupdate is fired. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| ASSERT_GE(WebMediaPlayer()->CurrentTime(), 0.3); |
| Video()->setCurrentTime(0.2); |
| WebMediaPlayer()->FinishSeek(); |
| |
| // Expect another timeupdate after FinishSeek due to |
| // seeking -> begin scrubbing -> pause -> timeupdate. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| platform()->RunUntilIdle(); |
| |
| // Advancing the remainder of the last periodic timeupdate interval should be |
| // insufficient to trigger a new timeupdate event because the seek's |
| // timeupdate occurred only 125ms ago. We desire to fire periodic timeupdates |
| // exactly every 250ms from the last timeupdate, and the seek's timeupdate |
| // should reset that 250ms ms countdown. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(0); |
| platform()->RunForPeriodSeconds(.125); |
| platform()->RunUntilIdle(); |
| |
| // Advancing another 125ms, we should expect a new timeupdate because we are |
| // now 250ms from the seek's timeupdate. |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| platform()->RunForPeriodSeconds(.125); |
| platform()->RunUntilIdle(); |
| |
| // Advancing 250ms further, we should expect yet another timeupdate because |
| // this represents a full periodic timeupdate interval with no interruptions |
| // (e.g. no-seeks). |
| EXPECT_CALL(*timeupdate_handler, Invoke(_, _)).Times(1); |
| platform()->RunForPeriodSeconds(.250); |
| platform()->RunUntilIdle(); |
| } |
| |
| TEST_F(HTMLMediaElementWithMockSchedulerTest, ShowPosterFlag_FalseAfterLoop) { |
| testing::InSequence dummy; |
| |
| // Adjust the duration of the media to something we can reasonably loop |
| SetMediaDuration(10.0); |
| |
| // Create a looping video with a source |
| GetDocument().body()->setInnerHTML( |
| "<video loop src=\"http://example.com\"></video>"); |
| platform()->RunUntilIdle(); |
| EXPECT_NE(WebMediaPlayer(), nullptr); |
| EXPECT_EQ(WebMediaPlayer()->Duration(), 10.0); |
| EXPECT_TRUE(Video()->Loop()); |
| |
| SimulateNetworkState(HTMLMediaElement::kNetworkIdle); |
| SimulateReadyState(HTMLMediaElement::kHaveEnoughData); |
| |
| // Simulate advancing playback time to enable periodic timeupdates. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta( |
| base::TimeDelta::FromMilliseconds(8)); |
| Video()->Play(); |
| |
| // Ensure the 'seeking' and 'seeked' events are fired, so we know a loop |
| // occurred |
| auto* seeking_handler = MakeGarbageCollected<MockEventListener>(); |
| EXPECT_CALL(*seeking_handler, Invoke(_, _)).Times(1); |
| Video()->addEventListener(event_type_names::kSeeking, seeking_handler); |
| platform()->RunForPeriodSeconds(15); |
| testing::Mock::VerifyAndClearExpectations(seeking_handler); |
| |
| auto* seeked_handler = MakeGarbageCollected<MockEventListener>(); |
| EXPECT_CALL(*seeked_handler, Invoke(_, _)).Times(1); |
| Video()->addEventListener(event_type_names::kSeeked, seeked_handler); |
| WebMediaPlayer()->FinishSeek(); |
| platform()->RunUntilIdle(); |
| testing::Mock::VerifyAndClearExpectations(seeked_handler); |
| |
| // ShowPosterFlag should be false after looping |
| EXPECT_FALSE(Video()->IsShowPosterFlagSet()); |
| } |
| |
| TEST_F(HTMLMediaElementWithMockSchedulerTest, ShowPosterFlag_FalseAfterEnded) { |
| testing::InSequence dummy; |
| |
| // Adjust the duration of the media to something we can reach the end of |
| SetMediaDuration(10.0); |
| |
| // Create a video with a source |
| GetDocument().body()->setInnerHTML( |
| "<video src=\"http://example.com\"></video>"); |
| platform()->RunUntilIdle(); |
| EXPECT_NE(WebMediaPlayer(), nullptr); |
| EXPECT_EQ(WebMediaPlayer()->Duration(), 10.0); |
| |
| SimulateNetworkState(HTMLMediaElement::kNetworkIdle); |
| SimulateReadyState(HTMLMediaElement::kHaveEnoughData); |
| |
| // Simulate advancing playback time to enable periodic timeupdates. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta( |
| base::TimeDelta::FromMilliseconds(8)); |
| Video()->Play(); |
| |
| // Ensure the 'ended' event is fired |
| auto* ended_handler = MakeGarbageCollected<MockEventListener>(); |
| Video()->addEventListener(event_type_names::kEnded, ended_handler); |
| |
| EXPECT_CALL(*ended_handler, Invoke(_, _)).Times(1); |
| platform()->RunForPeriodSeconds(15); |
| testing::Mock::VerifyAndClearExpectations(ended_handler); |
| |
| // ShowPosterFlag should be false even after ending |
| EXPECT_FALSE(Video()->IsShowPosterFlagSet()); |
| } |
| |
| struct TestCue { |
| double start_time; |
| double end_time; |
| char const* text; |
| }; |
| |
| constexpr TestCue kTestCueData[] = { |
| {15.000, 17.950, "At the left we can see..."}, |
| {18.160, 20.080, "At the right we can see the..."}, |
| {20.110, 21.960, "...the head-snarlers"}, |
| {21.990, 24.360, "Everything is safe.\nPerfectly safe."}, |
| {24.580, 27.030, "Emo?"}, |
| {28.200, 29.990, "Watch out!"}, |
| {47.030, 48.490, "Are you hurt?"}, |
| {51.990, 53.940, "I don't think so.\nYou?"}, |
| {55.160, 56.980, "I'm Ok."}, |
| {57.110, 61.110, "Get up.\nEmo, it's not safe here."}, |
| {62.030, 63.570, "Let's go."}, |
| }; |
| constexpr base::TimeDelta kTestCueDataLength = |
| base::TimeDelta::FromSecondsD(65); |
| |
| class CueEventListener final : public NativeEventListener { |
| public: |
| void Invoke(ExecutionContext* ctx, Event* event) override { |
| if (event->type() == event_type_names::kEnter) { |
| EXPECT_TRUE(event->target()->GetWrapperTypeInfo()->Equals( |
| VTTCue::GetStaticWrapperTypeInfo())); |
| auto* const cue = static_cast<VTTCue*>(event->target()); |
| auto* const media_element = cue->track()->MediaElement(); |
| |
| OnCueEnter(media_element, cue); |
| return; |
| } else if (event->type() == event_type_names::kExit) { |
| EXPECT_TRUE(event->target()->GetWrapperTypeInfo()->Equals( |
| VTTCue::GetStaticWrapperTypeInfo())); |
| auto* const cue = static_cast<VTTCue*>(event->target()); |
| auto* const media_element = cue->track()->MediaElement(); |
| |
| OnCueExit(media_element, cue); |
| return; |
| } |
| |
| // The above checks should be exhaustive |
| FAIL(); |
| } |
| |
| void ExpectAllEventsFiredWithinMargin(base::TimeDelta margin) const { |
| for (auto const& delta : cue_event_deltas_) { |
| EXPECT_TRUE(delta.enter_time_delta.has_value()); |
| EXPECT_LE(delta.enter_time_delta.value(), margin); |
| EXPECT_GE(delta.enter_time_delta.value(), base::TimeDelta()); |
| EXPECT_TRUE(delta.exit_time_delta.has_value()); |
| EXPECT_GE(delta.exit_time_delta.value(), base::TimeDelta()); |
| EXPECT_LE(delta.exit_time_delta.value(), margin); |
| } |
| } |
| |
| private: |
| struct CueChangeEventTimeDelta { |
| // The difference between when the cue was scheduled to begin and when the |
| // |kEnter| event was fired. The optional will be empty if the |kEnter| |
| // event was never fired. |
| base::Optional<base::TimeDelta> enter_time_delta; |
| |
| // The difference between when the cue was scheduled to end and when the |
| // |kExit| event fired. The optional will be empty if the |kExit| event |
| // was never fired. |
| base::Optional<base::TimeDelta> exit_time_delta; |
| }; |
| |
| void OnCueEnter(HTMLMediaElement* media_element, VTTCue* cue) { |
| auto const cue_index = cue->CueIndex(); |
| EXPECT_LE(cue_index, cue_event_deltas_.size()); |
| EXPECT_FALSE(cue_event_deltas_[cue_index].enter_time_delta.has_value()); |
| |
| // Get the start time delta |
| double const diff_seconds = media_element->currentTime() - cue->startTime(); |
| cue_event_deltas_[cue_index].enter_time_delta = |
| base::TimeDelta::FromSecondsD(diff_seconds); |
| } |
| |
| void OnCueExit(HTMLMediaElement* media_element, VTTCue* cue) { |
| auto const cue_index = cue->CueIndex(); |
| EXPECT_LE(cue_index, cue_event_deltas_.size()); |
| EXPECT_FALSE(cue_event_deltas_[cue_index].exit_time_delta.has_value()); |
| |
| // Get the end time delta |
| double const diff_seconds = |
| std::fabs(media_element->currentTime() - cue->endTime()); |
| cue_event_deltas_[cue_index].exit_time_delta = |
| base::TimeDelta::FromSecondsD(diff_seconds); |
| } |
| |
| std::array<CueChangeEventTimeDelta, base::size(kTestCueData)> |
| cue_event_deltas_; |
| }; |
| |
| TEST_F(HTMLMediaElementWithMockSchedulerTest, CueEnterExitEventLatency) { |
| testing::InSequence dummy; |
| GetDocument().body()->setInnerHTML("<video></video>"); |
| |
| // Set a src to trigger WebMediaPlayer creation. |
| Video()->SetSrc("http://example.com"); |
| |
| platform()->RunUntilIdle(); |
| ASSERT_NE(WebMediaPlayer(), nullptr); |
| |
| // Create a text track, and fill it with cue data |
| auto* text_track = |
| Video()->addTextTrack("subtitles", "", "", ASSERT_NO_EXCEPTION); |
| |
| auto* listener = MakeGarbageCollected<CueEventListener>(); |
| for (auto cue_data : kTestCueData) { |
| VTTCue* cue = MakeGarbageCollected<VTTCue>( |
| GetDocument(), cue_data.start_time, cue_data.end_time, cue_data.text); |
| text_track->addCue(cue); |
| cue->setOnenter(listener); |
| cue->setOnexit(listener); |
| } |
| |
| // Simulate conditions where playback is possible. |
| SimulateNetworkState(HTMLMediaElement::kNetworkIdle); |
| SimulateReadyState(HTMLMediaElement::kHaveFutureData); |
| |
| // Simulate advancing playback time to enable periodic timeupdates. |
| WebMediaPlayer()->SetAutoIncrementTimeDelta( |
| base::TimeDelta::FromMilliseconds(8)); |
| Video()->Play(); |
| |
| platform()->RunForPeriod(kTestCueDataLength); |
| platform()->RunUntilIdle(); |
| |
| // Ensure all cue events fired when expected with a 20ms tolerance |
| // As suggested by the spec: |
| // https://html.spec.whatwg.org/multipage/media.html#playing-the-media-resource:current-playback-position-13 |
| listener->ExpectAllEventsFiredWithinMargin( |
| base::TimeDelta::FromMilliseconds(20)); |
| } |
| |
| } // namespace blink |