blob: 94d83150b5d10d782652453b2b7b74d591b26a8c [file] [log] [blame]
// Copyright 2020 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/modules/mediastream/media_stream_track_processor.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "media/base/video_frame.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/public/web/modules/mediastream/media_stream_video_source.h"
#include "third_party/blink/public/web/web_heap.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_stream_track_signal.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/core/streams/readable_stream_default_reader.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/core/streams/writable_stream_default_writer.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_track.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_track_generator.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_track_generator_init.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_video_track.h"
#include "third_party/blink/renderer/modules/mediastream/mock_media_stream_audio_sink.h"
#include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_sink.h"
#include "third_party/blink/renderer/modules/mediastream/mock_media_stream_video_source.h"
#include "third_party/blink/renderer/modules/mediastream/pushable_media_stream_audio_source.h"
#include "third_party/blink/renderer/modules/mediastream/pushable_media_stream_video_source.h"
#include "third_party/blink/renderer/modules/mediastream/stream_test_utils.h"
#include "third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_audio_source.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_audio_track.h"
#include "third_party/blink/renderer/platform/mediastream/media_stream_source.h"
#include "third_party/blink/renderer/platform/testing/io_task_runner_testing_platform_support.h"
using testing::_;
namespace blink {
namespace {
std::unique_ptr<PushableMediaStreamAudioSource> CreatePushableAudioSource() {
// Use the IO thread for testing purposes.
return std::make_unique<PushableMediaStreamAudioSource>(
Thread::MainThread()->GetTaskRunner(),
Platform::Current()->GetIOTaskRunner());
}
PushableMediaStreamVideoSource* CreatePushableVideoSource() {
PushableMediaStreamVideoSource* pushable_video_source =
new PushableMediaStreamVideoSource();
MediaStreamSource* media_stream_source =
MakeGarbageCollected<MediaStreamSource>(
"source_id", MediaStreamSource::kTypeVideo, "source_name",
/*remote=*/false);
media_stream_source->SetPlatformSource(
base::WrapUnique(pushable_video_source));
return pushable_video_source;
}
MediaStreamTrack* CreateAudioMediaStreamTrack(
ExecutionContext* context,
std::unique_ptr<MediaStreamAudioSource> source) {
auto* source_ptr = source.get();
MediaStreamSource* media_stream_source =
MakeGarbageCollected<MediaStreamSource>(
"source_id", MediaStreamSource::kTypeAudio, "source_name",
/*remote=*/false);
media_stream_source->SetPlatformSource(std::move(source));
MediaStreamComponent* component =
MakeGarbageCollected<MediaStreamComponent>(media_stream_source);
source_ptr->ConnectToTrack(component);
return MakeGarbageCollected<MediaStreamTrack>(context, component);
}
ScriptValue CreateRequestFrameChunk(ScriptState* script_state) {
MediaStreamTrackSignal* signal = MediaStreamTrackSignal::Create();
signal->setSignalType("request-frame");
return ScriptValue(script_state->GetIsolate(),
ToV8(signal, script_state->GetContext()->Global(),
script_state->GetIsolate()));
}
ScriptValue CreateSetMinFrameRateChunk(ScriptState* script_state,
double frame_rate) {
MediaStreamTrackSignal* signal = MediaStreamTrackSignal::Create();
signal->setSignalType("set-min-frame-rate");
signal->setFrameRate(frame_rate);
return ScriptValue(script_state->GetIsolate(),
ToV8(signal, script_state->GetContext()->Global(),
script_state->GetIsolate()));
}
ScriptValue CreateInvalidSignalChunk(ScriptState* script_state) {
MediaStreamTrackSignal* signal = MediaStreamTrackSignal::Create();
signal->setSignalType("set-min-frame-rate");
return ScriptValue(script_state->GetIsolate(),
ToV8(signal, script_state->GetContext()->Global(),
script_state->GetIsolate()));
}
} // namespace
class MediaStreamTrackProcessorTest : public testing::Test {
public:
~MediaStreamTrackProcessorTest() override {
platform_->RunUntilIdle();
WebHeap::CollectAllGarbageForTesting();
}
protected:
ScopedTestingPlatformSupport<IOTaskRunnerTestingPlatformSupport> platform_;
};
TEST_F(MediaStreamTrackProcessorTest, VideoFramesAreExposed) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateVideoMediaStreamTrack(v8_scope.GetExecutionContext(),
pushable_video_source),
exception_state);
EXPECT_FALSE(exception_state.HadException());
EXPECT_EQ(
track_processor->InputTrack()->Component()->Source()->GetPlatformSource(),
pushable_video_source);
MockMediaStreamVideoSink mock_video_sink;
mock_video_sink.ConnectToTrack(
WebMediaStreamTrack(track_processor->InputTrack()->Component()));
EXPECT_EQ(mock_video_sink.number_of_frames(), 0);
EXPECT_EQ(mock_video_sink.last_frame(), nullptr);
auto* reader =
track_processor->readable(script_state)
->GetDefaultReaderForTesting(script_state, exception_state);
EXPECT_FALSE(exception_state.HadException());
// Deliver a frame
base::RunLoop sink_loop;
EXPECT_CALL(mock_video_sink, OnVideoFrame(_))
.WillOnce(base::test::RunOnceClosure(sink_loop.QuitClosure()));
scoped_refptr<media::VideoFrame> frame =
media::VideoFrame::CreateBlackFrame(gfx::Size(10, 5));
pushable_video_source->PushFrame(frame, base::TimeTicks());
ScriptPromiseTester read_tester(script_state,
reader->read(script_state, exception_state));
EXPECT_FALSE(read_tester.IsFulfilled());
read_tester.WaitUntilSettled();
EXPECT_FALSE(exception_state.HadException());
EXPECT_TRUE(read_tester.IsFulfilled());
EXPECT_TRUE(read_tester.Value().IsObject());
sink_loop.Run();
EXPECT_EQ(mock_video_sink.number_of_frames(), 1);
EXPECT_EQ(mock_video_sink.last_frame(), frame);
}
TEST_F(MediaStreamTrackProcessorTest, AudioFramesAreExposed) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
std::unique_ptr<PushableMediaStreamAudioSource> pushable_audio_source =
CreatePushableAudioSource();
auto* pushable_source_ptr = pushable_audio_source.get();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateAudioMediaStreamTrack(v8_scope.GetExecutionContext(),
std::move(pushable_audio_source)),
exception_state);
EXPECT_FALSE(exception_state.HadException());
MediaStreamComponent* component = track_processor->InputTrack()->Component();
EXPECT_EQ(component->Source()->GetPlatformSource(), pushable_source_ptr);
MockMediaStreamAudioSink mock_audio_sink;
WebMediaStreamAudioSink::AddToAudioTrack(&mock_audio_sink,
WebMediaStreamTrack(component));
auto* reader =
track_processor->readable(script_state)
->GetDefaultReaderForTesting(script_state, exception_state);
EXPECT_FALSE(exception_state.HadException());
// Deliver a frame.
base::RunLoop sink_loop;
EXPECT_CALL(mock_audio_sink, OnData(_, _))
.WillOnce(base::test::RunOnceClosure(sink_loop.QuitClosure()));
pushable_source_ptr->PushAudioData(AudioFrameSerializationData::Wrap(
media::AudioBus::Create(/*channels=*/2, /*frames=*/100),
/*sample_rate=*/8000, base::TimeDelta::FromSeconds(1)));
ScriptPromiseTester read_tester(script_state,
reader->read(script_state, exception_state));
EXPECT_FALSE(read_tester.IsFulfilled());
read_tester.WaitUntilSettled();
EXPECT_FALSE(exception_state.HadException());
EXPECT_TRUE(read_tester.IsFulfilled());
EXPECT_TRUE(read_tester.Value().IsObject());
sink_loop.Run();
WebMediaStreamAudioSink::RemoveFromAudioTrack(&mock_audio_sink,
WebMediaStreamTrack(component));
}
TEST_F(MediaStreamTrackProcessorTest, VideoControlSignalsAreForwarded) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
MockMediaStreamVideoSource* mock_video_source = CreateMockVideoSource();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateVideoMediaStreamTrack(v8_scope.GetExecutionContext(),
mock_video_source),
exception_state);
EXPECT_FALSE(exception_state.HadException());
EXPECT_EQ(
track_processor->InputTrack()->Component()->Source()->GetPlatformSource(),
mock_video_source);
mock_video_source->StartMockedSource();
EXPECT_CALL(*mock_video_source, OnRequestRefreshFrame());
auto* writer = track_processor->writableControl(script_state)
->getWriter(script_state, exception_state);
ScriptPromiseTester request_frame_tester(
script_state,
writer->write(script_state, CreateRequestFrameChunk(script_state),
exception_state));
request_frame_tester.WaitUntilSettled();
EXPECT_TRUE(request_frame_tester.IsFulfilled());
EXPECT_FALSE(exception_state.HadException());
MediaStreamVideoTrack* platform_track =
MediaStreamVideoTrack::From(track_processor->InputTrack()->Component());
EXPECT_FALSE(platform_track->min_frame_rate().has_value());
const double min_frame_rate = 15.0;
ScriptPromiseTester set_min_frame_rate_tester(
script_state,
writer->write(script_state,
CreateSetMinFrameRateChunk(script_state, min_frame_rate),
exception_state));
set_min_frame_rate_tester.WaitUntilSettled();
EXPECT_TRUE(set_min_frame_rate_tester.IsFulfilled());
EXPECT_FALSE(exception_state.HadException());
EXPECT_TRUE(platform_track->min_frame_rate().has_value());
EXPECT_EQ(platform_track->min_frame_rate().value(), min_frame_rate);
ScriptPromiseTester invalid_signal_tester(
script_state,
writer->write(script_state, CreateInvalidSignalChunk(script_state),
exception_state));
invalid_signal_tester.WaitUntilSettled();
EXPECT_TRUE(invalid_signal_tester.IsRejected());
}
TEST_F(MediaStreamTrackProcessorTest, AudioControlSignalsAreRejected) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateAudioMediaStreamTrack(v8_scope.GetExecutionContext(),
std::make_unique<MediaStreamAudioSource>(
Thread::MainThread()->GetTaskRunner(),
/*is_local=*/true)),
exception_state);
auto* writer = track_processor->writableControl(script_state)
->getWriter(script_state, exception_state);
ScriptPromiseTester tester(
script_state,
writer->write(script_state, CreateRequestFrameChunk(script_state),
exception_state));
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsRejected());
}
TEST_F(MediaStreamTrackProcessorTest, CanceledReadableDisconnects) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
ExceptionState& exception_state = v8_scope.GetExceptionState();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateVideoMediaStreamTrack(v8_scope.GetExecutionContext(),
pushable_video_source),
exception_state);
// Initially the track has no sinks.
MediaStreamVideoTrack* video_track =
MediaStreamVideoTrack::From(track_processor->InputTrack()->Component());
EXPECT_EQ(video_track->CountSinks(), 0u);
MockMediaStreamVideoSink mock_video_sink;
mock_video_sink.ConnectToTrack(
WebMediaStreamTrack(track_processor->InputTrack()->Component()));
EXPECT_EQ(mock_video_sink.number_of_frames(), 0);
EXPECT_EQ(mock_video_sink.last_frame(), nullptr);
EXPECT_EQ(video_track->CountSinks(), 1u);
// Accessing the readable connects it to the track
auto* readable = track_processor->readable(script_state);
EXPECT_EQ(video_track->CountSinks(), 2u);
ScriptPromiseTester cancel_tester(
script_state, readable->cancel(script_state, exception_state));
cancel_tester.WaitUntilSettled();
EXPECT_FALSE(exception_state.HadException());
EXPECT_EQ(video_track->CountSinks(), 1u);
// Cancelling the readable does not stop the track.
// Push a frame and expect delivery to the mock sink.
base::RunLoop sink_loop;
EXPECT_CALL(mock_video_sink, OnVideoFrame(_))
.WillOnce(base::test::RunOnceClosure(sink_loop.QuitClosure()));
scoped_refptr<media::VideoFrame> frame =
media::VideoFrame::CreateBlackFrame(gfx::Size(10, 5));
pushable_video_source->PushFrame(frame, base::TimeTicks());
sink_loop.Run();
EXPECT_EQ(mock_video_sink.number_of_frames(), 1);
EXPECT_EQ(mock_video_sink.last_frame(), frame);
}
TEST_F(MediaStreamTrackProcessorTest, ProcessorConnectsToGenerator) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
// Create a processor connected to a pushable source.
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
ExceptionState& exception_state = v8_scope.GetExceptionState();
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(
script_state,
CreateVideoMediaStreamTrack(v8_scope.GetExecutionContext(),
pushable_video_source),
exception_state);
// Create generator and connect it to a mock sink.
MediaStreamTrackGeneratorInit* init = MediaStreamTrackGeneratorInit::Create();
init->setKind("video");
MediaStreamTrackGenerator* track_generator =
MediaStreamTrackGenerator::Create(script_state, init, exception_state);
MockMediaStreamVideoSink mock_video_sink;
mock_video_sink.ConnectToTrack(
WebMediaStreamTrack(track_generator->Component()));
EXPECT_EQ(mock_video_sink.number_of_frames(), 0);
EXPECT_EQ(mock_video_sink.last_frame(), nullptr);
// Connect the processor to the generator
track_processor->readable(script_state)
->pipeTo(script_state, track_generator->writable(script_state),
exception_state);
// Push a frame and verify that it makes it to the sink at the end of the
// chain.
base::RunLoop sink_loop;
EXPECT_CALL(mock_video_sink, OnVideoFrame(_))
.WillOnce(base::test::RunOnceClosure(sink_loop.QuitClosure()));
scoped_refptr<media::VideoFrame> frame =
media::VideoFrame::CreateBlackFrame(gfx::Size(10, 5));
pushable_video_source->PushFrame(frame, base::TimeTicks());
sink_loop.Run();
EXPECT_EQ(mock_video_sink.number_of_frames(), 1);
EXPECT_EQ(mock_video_sink.last_frame(), frame);
}
TEST_F(MediaStreamTrackProcessorTest, NullInputTrack) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
MediaStreamTrack* track = nullptr;
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
EXPECT_EQ(track_processor, nullptr);
EXPECT_TRUE(exception_state.HadException());
EXPECT_EQ(static_cast<ESErrorType>(v8_scope.GetExceptionState().Code()),
ESErrorType::kTypeError);
}
TEST_F(MediaStreamTrackProcessorTest, EndedTrack) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
MediaStreamTrack* track = CreateVideoMediaStreamTrack(
v8_scope.GetExecutionContext(), pushable_video_source);
track->stopTrack(v8_scope.GetExecutionContext());
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
EXPECT_EQ(track_processor, nullptr);
EXPECT_TRUE(exception_state.HadException());
EXPECT_EQ(static_cast<ESErrorType>(v8_scope.GetExceptionState().Code()),
ESErrorType::kTypeError);
}
TEST_F(MediaStreamTrackProcessorTest, VideoCloseOnTrackEnd) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
MediaStreamTrack* track = CreateVideoMediaStreamTrack(
v8_scope.GetExecutionContext(), pushable_video_source);
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
ReadableStream* readable = track_processor->readable(script_state);
EXPECT_FALSE(readable->IsClosed());
track->stopTrack(v8_scope.GetExecutionContext());
EXPECT_TRUE(readable->IsClosed());
}
TEST_F(MediaStreamTrackProcessorTest, VideoNoCloseOnTrackDisable) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
PushableMediaStreamVideoSource* pushable_video_source =
CreatePushableVideoSource();
MediaStreamTrack* track = CreateVideoMediaStreamTrack(
v8_scope.GetExecutionContext(), pushable_video_source);
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
ReadableStream* readable = track_processor->readable(script_state);
EXPECT_FALSE(readable->IsClosed());
track->setEnabled(false);
EXPECT_FALSE(readable->IsClosed());
}
TEST_F(MediaStreamTrackProcessorTest, AudioCloseOnTrackEnd) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
std::unique_ptr<PushableMediaStreamAudioSource> pushable_audio_source =
CreatePushableAudioSource();
MediaStreamTrack* track = CreateAudioMediaStreamTrack(
v8_scope.GetExecutionContext(), std::move(pushable_audio_source));
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
ReadableStream* readable = track_processor->readable(script_state);
EXPECT_FALSE(readable->IsClosed());
track->stopTrack(v8_scope.GetExecutionContext());
EXPECT_TRUE(readable->IsClosed());
}
TEST_F(MediaStreamTrackProcessorTest, AudioNoCloseOnTrackDisable) {
V8TestingScope v8_scope;
ScriptState* script_state = v8_scope.GetScriptState();
ExceptionState& exception_state = v8_scope.GetExceptionState();
std::unique_ptr<PushableMediaStreamAudioSource> pushable_audio_source =
CreatePushableAudioSource();
MediaStreamTrack* track = CreateAudioMediaStreamTrack(
v8_scope.GetExecutionContext(), std::move(pushable_audio_source));
MediaStreamTrackProcessor* track_processor =
MediaStreamTrackProcessor::Create(script_state, track, exception_state);
ReadableStream* readable = track_processor->readable(script_state);
EXPECT_FALSE(readable->IsClosed());
track->setEnabled(false);
EXPECT_FALSE(readable->IsClosed());
}
} // namespace blink