mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-13 05:40:42 +01:00
Reland "FrameCadenceAdapter: align video encoding to metronome"
This is a reland of commit b39c2a8464
Original change's description:
> FrameCadenceAdapter: align video encoding to metronome
>
> This CL aligns the video encoding tasks to metronome tick which
> similar with the metronome decoding.
>
> Design doc: https://docs.google.com/document/d/18PvEgS-DehClK6twCSCATOlX-j9acmXd-3vjb0tR9-Y
>
> Bug: b/304158952
> Change-Id: I262bd4a5097fdaeed559b9d7391a059ae86e2d63
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/327460
> Reviewed-by: Markus Handell <handellm@webrtc.org>
> Reviewed-by: Harald Alvestrand <hta@webrtc.org>
> Reviewed-by: Henrik Boström <hbos@webrtc.org>
> Commit-Queue: Zhaoliang Ma <zhaoliang.ma@intel.com>
> Cr-Commit-Position: refs/heads/main@{#41469}
Bug: b/304158952
Change-Id: Icf4e1ad91f5c98f3c32a88ffe4d6277e907353e6
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/333464
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Markus Handell <handellm@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#41479}
This commit is contained in:
parent
55a61898a8
commit
f089d7ea54
14 changed files with 310 additions and 23 deletions
|
@ -49,6 +49,10 @@ void ForcedTickMetronome::Tick() {
|
||||||
FakeMetronome::FakeMetronome(TimeDelta tick_period)
|
FakeMetronome::FakeMetronome(TimeDelta tick_period)
|
||||||
: tick_period_(tick_period) {}
|
: tick_period_(tick_period) {}
|
||||||
|
|
||||||
|
void FakeMetronome::SetTickPeriod(TimeDelta tick_period) {
|
||||||
|
tick_period_ = tick_period;
|
||||||
|
}
|
||||||
|
|
||||||
void FakeMetronome::RequestCallOnNextTick(
|
void FakeMetronome::RequestCallOnNextTick(
|
||||||
absl::AnyInvocable<void() &&> callback) {
|
absl::AnyInvocable<void() &&> callback) {
|
||||||
TaskQueueBase* current = TaskQueueBase::Current();
|
TaskQueueBase* current = TaskQueueBase::Current();
|
||||||
|
|
|
@ -55,12 +55,14 @@ class FakeMetronome : public Metronome {
|
||||||
public:
|
public:
|
||||||
explicit FakeMetronome(TimeDelta tick_period);
|
explicit FakeMetronome(TimeDelta tick_period);
|
||||||
|
|
||||||
|
void SetTickPeriod(TimeDelta tick_period);
|
||||||
|
|
||||||
// Metronome implementation.
|
// Metronome implementation.
|
||||||
void RequestCallOnNextTick(absl::AnyInvocable<void() &&> callback) override;
|
void RequestCallOnNextTick(absl::AnyInvocable<void() &&> callback) override;
|
||||||
TimeDelta TickPeriod() const override;
|
TimeDelta TickPeriod() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const TimeDelta tick_period_;
|
TimeDelta tick_period_;
|
||||||
std::vector<absl::AnyInvocable<void() &&>> callbacks_;
|
std::vector<absl::AnyInvocable<void() &&>> callbacks_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1442,6 +1442,10 @@ struct RTC_EXPORT PeerConnectionFactoryDependencies final {
|
||||||
transport_controller_send_factory;
|
transport_controller_send_factory;
|
||||||
// Metronome used for decoding, must be called on the worker thread.
|
// Metronome used for decoding, must be called on the worker thread.
|
||||||
std::unique_ptr<Metronome> decode_metronome;
|
std::unique_ptr<Metronome> decode_metronome;
|
||||||
|
// Metronome used for encoding, must be called on the worker thread.
|
||||||
|
// TODO(b/304158952): Consider merging into a single metronome for all codec
|
||||||
|
// usage.
|
||||||
|
std::unique_ptr<Metronome> encode_metronome;
|
||||||
|
|
||||||
// Media specific dependencies. Unused when `media_factory == nullptr`.
|
// Media specific dependencies. Unused when `media_factory == nullptr`.
|
||||||
rtc::scoped_refptr<AudioDeviceModule> adm;
|
rtc::scoped_refptr<AudioDeviceModule> adm;
|
||||||
|
|
|
@ -889,10 +889,11 @@ webrtc::VideoSendStream* Call::CreateVideoSendStream(
|
||||||
VideoSendStream* send_stream = new VideoSendStream(
|
VideoSendStream* send_stream = new VideoSendStream(
|
||||||
&env_.clock(), num_cpu_cores_, &env_.task_queue_factory(),
|
&env_.clock(), num_cpu_cores_, &env_.task_queue_factory(),
|
||||||
network_thread_, call_stats_->AsRtcpRttStats(), transport_send_.get(),
|
network_thread_, call_stats_->AsRtcpRttStats(), transport_send_.get(),
|
||||||
bitrate_allocator_.get(), video_send_delay_stats_.get(),
|
config_.encode_metronome, bitrate_allocator_.get(),
|
||||||
&env_.event_log(), std::move(config), std::move(encoder_config),
|
video_send_delay_stats_.get(), &env_.event_log(), std::move(config),
|
||||||
suspended_video_send_ssrcs_, suspended_video_payload_states_,
|
std::move(encoder_config), suspended_video_send_ssrcs_,
|
||||||
std::move(fec_controller), env_.field_trials());
|
suspended_video_payload_states_, std::move(fec_controller),
|
||||||
|
env_.field_trials());
|
||||||
|
|
||||||
for (uint32_t ssrc : ssrcs) {
|
for (uint32_t ssrc : ssrcs) {
|
||||||
RTC_DCHECK(video_send_ssrcs_.find(ssrc) == video_send_ssrcs_.end());
|
RTC_DCHECK(video_send_ssrcs_.find(ssrc) == video_send_ssrcs_.end());
|
||||||
|
|
|
@ -69,6 +69,7 @@ struct CallConfig {
|
||||||
rtp_transport_controller_send_factory = nullptr;
|
rtp_transport_controller_send_factory = nullptr;
|
||||||
|
|
||||||
Metronome* decode_metronome = nullptr;
|
Metronome* decode_metronome = nullptr;
|
||||||
|
Metronome* encode_metronome = nullptr;
|
||||||
|
|
||||||
// The burst interval of the pacer, see TaskQueuePacedSender constructor.
|
// The burst interval of the pacer, see TaskQueuePacedSender constructor.
|
||||||
absl::optional<TimeDelta> pacer_burst_interval;
|
absl::optional<TimeDelta> pacer_burst_interval;
|
||||||
|
|
|
@ -103,7 +103,8 @@ PeerConnectionFactory::PeerConnectionFactory(
|
||||||
(dependencies->transport_controller_send_factory)
|
(dependencies->transport_controller_send_factory)
|
||||||
? std::move(dependencies->transport_controller_send_factory)
|
? std::move(dependencies->transport_controller_send_factory)
|
||||||
: std::make_unique<RtpTransportControllerSendFactory>()),
|
: std::make_unique<RtpTransportControllerSendFactory>()),
|
||||||
decode_metronome_(std::move(dependencies->decode_metronome)) {}
|
decode_metronome_(std::move(dependencies->decode_metronome)),
|
||||||
|
encode_metronome_(std::move(dependencies->encode_metronome)) {}
|
||||||
|
|
||||||
PeerConnectionFactory::PeerConnectionFactory(
|
PeerConnectionFactory::PeerConnectionFactory(
|
||||||
PeerConnectionFactoryDependencies dependencies)
|
PeerConnectionFactoryDependencies dependencies)
|
||||||
|
@ -119,6 +120,7 @@ PeerConnectionFactory::~PeerConnectionFactory() {
|
||||||
worker_thread()->BlockingCall([this] {
|
worker_thread()->BlockingCall([this] {
|
||||||
RTC_DCHECK_RUN_ON(worker_thread());
|
RTC_DCHECK_RUN_ON(worker_thread());
|
||||||
decode_metronome_ = nullptr;
|
decode_metronome_ = nullptr;
|
||||||
|
encode_metronome_ = nullptr;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,6 +346,7 @@ std::unique_ptr<Call> PeerConnectionFactory::CreateCall_w(
|
||||||
call_config.rtp_transport_controller_send_factory =
|
call_config.rtp_transport_controller_send_factory =
|
||||||
transport_controller_send_factory_.get();
|
transport_controller_send_factory_.get();
|
||||||
call_config.decode_metronome = decode_metronome_.get();
|
call_config.decode_metronome = decode_metronome_.get();
|
||||||
|
call_config.encode_metronome = encode_metronome_.get();
|
||||||
call_config.pacer_burst_interval = configuration.pacer_burst_interval;
|
call_config.pacer_burst_interval = configuration.pacer_burst_interval;
|
||||||
return context_->call_factory()->CreateCall(call_config);
|
return context_->call_factory()->CreateCall(call_config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,6 +148,7 @@ class PeerConnectionFactory : public PeerConnectionFactoryInterface {
|
||||||
const std::unique_ptr<RtpTransportControllerSendFactoryInterface>
|
const std::unique_ptr<RtpTransportControllerSendFactoryInterface>
|
||||||
transport_controller_send_factory_;
|
transport_controller_send_factory_;
|
||||||
std::unique_ptr<Metronome> decode_metronome_ RTC_GUARDED_BY(worker_thread());
|
std::unique_ptr<Metronome> decode_metronome_ RTC_GUARDED_BY(worker_thread());
|
||||||
|
std::unique_ptr<Metronome> encode_metronome_ RTC_GUARDED_BY(worker_thread());
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace webrtc
|
} // namespace webrtc
|
||||||
|
|
|
@ -92,6 +92,7 @@ rtc_library("video") {
|
||||||
"../api/crypto:frame_decryptor_interface",
|
"../api/crypto:frame_decryptor_interface",
|
||||||
"../api/crypto:options",
|
"../api/crypto:options",
|
||||||
"../api/environment",
|
"../api/environment",
|
||||||
|
"../api/metronome",
|
||||||
"../api/task_queue",
|
"../api/task_queue",
|
||||||
"../api/task_queue:pending_task_safety_flag",
|
"../api/task_queue:pending_task_safety_flag",
|
||||||
"../api/units:data_rate",
|
"../api/units:data_rate",
|
||||||
|
@ -230,6 +231,7 @@ rtc_library("frame_cadence_adapter") {
|
||||||
deps = [
|
deps = [
|
||||||
"../api:field_trials_view",
|
"../api:field_trials_view",
|
||||||
"../api:sequence_checker",
|
"../api:sequence_checker",
|
||||||
|
"../api/metronome",
|
||||||
"../api/task_queue",
|
"../api/task_queue",
|
||||||
"../api/task_queue:pending_task_safety_flag",
|
"../api/task_queue:pending_task_safety_flag",
|
||||||
"../api/units:time_delta",
|
"../api/units:time_delta",
|
||||||
|
@ -253,6 +255,7 @@ rtc_library("frame_cadence_adapter") {
|
||||||
absl_deps = [
|
absl_deps = [
|
||||||
"//third_party/abseil-cpp/absl/algorithm:container",
|
"//third_party/abseil-cpp/absl/algorithm:container",
|
||||||
"//third_party/abseil-cpp/absl/base:core_headers",
|
"//third_party/abseil-cpp/absl/base:core_headers",
|
||||||
|
"//third_party/abseil-cpp/absl/cleanup:cleanup",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "absl/algorithm/container.h"
|
#include "absl/algorithm/container.h"
|
||||||
#include "absl/base/attributes.h"
|
#include "absl/base/attributes.h"
|
||||||
|
#include "absl/cleanup/cleanup.h"
|
||||||
#include "api/sequence_checker.h"
|
#include "api/sequence_checker.h"
|
||||||
#include "api/task_queue/pending_task_safety_flag.h"
|
#include "api/task_queue/pending_task_safety_flag.h"
|
||||||
#include "api/task_queue/task_queue_base.h"
|
#include "api/task_queue/task_queue_base.h"
|
||||||
|
@ -234,10 +235,81 @@ class ZeroHertzAdapterMode : public AdapterMode {
|
||||||
ScopedTaskSafety safety_;
|
ScopedTaskSafety safety_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Implements a frame cadence adapter supporting VSync aligned encoding.
|
||||||
|
class VSyncEncodeAdapterMode : public AdapterMode {
|
||||||
|
public:
|
||||||
|
VSyncEncodeAdapterMode(
|
||||||
|
Clock* clock,
|
||||||
|
TaskQueueBase* queue,
|
||||||
|
rtc::scoped_refptr<PendingTaskSafetyFlag> queue_safety_flag,
|
||||||
|
Metronome* metronome,
|
||||||
|
TaskQueueBase* worker_queue,
|
||||||
|
FrameCadenceAdapterInterface::Callback* callback)
|
||||||
|
: clock_(clock),
|
||||||
|
queue_(queue),
|
||||||
|
queue_safety_flag_(queue_safety_flag),
|
||||||
|
callback_(callback),
|
||||||
|
metronome_(metronome),
|
||||||
|
worker_queue_(worker_queue) {
|
||||||
|
queue_sequence_checker_.Detach();
|
||||||
|
worker_sequence_checker_.Detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter overrides.
|
||||||
|
void OnFrame(Timestamp post_time,
|
||||||
|
bool queue_overload,
|
||||||
|
const VideoFrame& frame) override;
|
||||||
|
|
||||||
|
absl::optional<uint32_t> GetInputFrameRateFps() override {
|
||||||
|
RTC_DCHECK_RUN_ON(&queue_sequence_checker_);
|
||||||
|
return input_framerate_.Rate(clock_->TimeInMilliseconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateFrameRate() override {
|
||||||
|
RTC_DCHECK_RUN_ON(&queue_sequence_checker_);
|
||||||
|
input_framerate_.Update(1, clock_->TimeInMilliseconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncodeAllEnqueuedFrames();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Holds input frames coming from the client ready to be encoded.
|
||||||
|
struct InputFrameRef {
|
||||||
|
InputFrameRef(const VideoFrame& video_frame, Timestamp time_when_posted_us)
|
||||||
|
: time_when_posted_us(time_when_posted_us),
|
||||||
|
video_frame(std::move(video_frame)) {}
|
||||||
|
Timestamp time_when_posted_us;
|
||||||
|
const VideoFrame video_frame;
|
||||||
|
};
|
||||||
|
|
||||||
|
Clock* const clock_;
|
||||||
|
TaskQueueBase* queue_;
|
||||||
|
RTC_NO_UNIQUE_ADDRESS SequenceChecker queue_sequence_checker_;
|
||||||
|
rtc::scoped_refptr<PendingTaskSafetyFlag> queue_safety_flag_;
|
||||||
|
// Input frame rate statistics for use when not in zero-hertz mode.
|
||||||
|
RateStatistics input_framerate_ RTC_GUARDED_BY(queue_sequence_checker_){
|
||||||
|
FrameCadenceAdapterInterface::kFrameRateAveragingWindowSizeMs, 1000};
|
||||||
|
FrameCadenceAdapterInterface::Callback* const callback_;
|
||||||
|
|
||||||
|
Metronome* metronome_;
|
||||||
|
TaskQueueBase* const worker_queue_;
|
||||||
|
RTC_NO_UNIQUE_ADDRESS SequenceChecker worker_sequence_checker_;
|
||||||
|
// `worker_safety_` protects tasks on the worker queue related to `metronome_`
|
||||||
|
// since metronome usage must happen on worker thread.
|
||||||
|
ScopedTaskSafetyDetached worker_safety_;
|
||||||
|
Timestamp expected_next_tick_ RTC_GUARDED_BY(worker_sequence_checker_) =
|
||||||
|
Timestamp::PlusInfinity();
|
||||||
|
// Vector of input frames to be encoded.
|
||||||
|
std::vector<InputFrameRef> input_queue_
|
||||||
|
RTC_GUARDED_BY(worker_sequence_checker_);
|
||||||
|
};
|
||||||
|
|
||||||
class FrameCadenceAdapterImpl : public FrameCadenceAdapterInterface {
|
class FrameCadenceAdapterImpl : public FrameCadenceAdapterInterface {
|
||||||
public:
|
public:
|
||||||
FrameCadenceAdapterImpl(Clock* clock,
|
FrameCadenceAdapterImpl(Clock* clock,
|
||||||
TaskQueueBase* queue,
|
TaskQueueBase* queue,
|
||||||
|
Metronome* metronome,
|
||||||
|
TaskQueueBase* worker_queue,
|
||||||
const FieldTrialsView& field_trials);
|
const FieldTrialsView& field_trials);
|
||||||
~FrameCadenceAdapterImpl();
|
~FrameCadenceAdapterImpl();
|
||||||
|
|
||||||
|
@ -273,6 +345,10 @@ class FrameCadenceAdapterImpl : public FrameCadenceAdapterInterface {
|
||||||
// - zero-hertz mode enabled
|
// - zero-hertz mode enabled
|
||||||
bool IsZeroHertzScreenshareEnabled() const RTC_RUN_ON(queue_);
|
bool IsZeroHertzScreenshareEnabled() const RTC_RUN_ON(queue_);
|
||||||
|
|
||||||
|
// Configures current adapter on non-ZeroHertz mode, called when Initialize or
|
||||||
|
// MaybeReconfigureAdapters.
|
||||||
|
void ConfigureCurrentAdapterWithoutZeroHertz();
|
||||||
|
|
||||||
// Handles adapter creation on configuration changes.
|
// Handles adapter creation on configuration changes.
|
||||||
void MaybeReconfigureAdapters(bool was_zero_hertz_enabled) RTC_RUN_ON(queue_);
|
void MaybeReconfigureAdapters(bool was_zero_hertz_enabled) RTC_RUN_ON(queue_);
|
||||||
|
|
||||||
|
@ -283,14 +359,21 @@ class FrameCadenceAdapterImpl : public FrameCadenceAdapterInterface {
|
||||||
// 0 Hz.
|
// 0 Hz.
|
||||||
const bool zero_hertz_screenshare_enabled_;
|
const bool zero_hertz_screenshare_enabled_;
|
||||||
|
|
||||||
// The two possible modes we're under.
|
// The three possible modes we're under.
|
||||||
absl::optional<PassthroughAdapterMode> passthrough_adapter_;
|
absl::optional<PassthroughAdapterMode> passthrough_adapter_;
|
||||||
absl::optional<ZeroHertzAdapterMode> zero_hertz_adapter_;
|
absl::optional<ZeroHertzAdapterMode> zero_hertz_adapter_;
|
||||||
|
// The `vsync_encode_adapter_` must be destroyed on the worker queue since
|
||||||
|
// VSync metronome needs to happen on worker thread.
|
||||||
|
std::unique_ptr<VSyncEncodeAdapterMode> vsync_encode_adapter_;
|
||||||
// If set, zero-hertz mode has been enabled.
|
// If set, zero-hertz mode has been enabled.
|
||||||
absl::optional<ZeroHertzModeParams> zero_hertz_params_;
|
absl::optional<ZeroHertzModeParams> zero_hertz_params_;
|
||||||
// Cache for the current adapter mode.
|
// Cache for the current adapter mode.
|
||||||
AdapterMode* current_adapter_mode_ = nullptr;
|
AdapterMode* current_adapter_mode_ = nullptr;
|
||||||
|
|
||||||
|
// VSync encoding is used when this valid.
|
||||||
|
Metronome* const metronome_;
|
||||||
|
TaskQueueBase* const worker_queue_;
|
||||||
|
|
||||||
// Timestamp for statistics reporting.
|
// Timestamp for statistics reporting.
|
||||||
absl::optional<Timestamp> zero_hertz_adapter_created_timestamp_
|
absl::optional<Timestamp> zero_hertz_adapter_created_timestamp_
|
||||||
RTC_GUARDED_BY(queue_);
|
RTC_GUARDED_BY(queue_);
|
||||||
|
@ -620,23 +703,98 @@ void ZeroHertzAdapterMode::MaybeStartRefreshFrameRequester() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VSyncEncodeAdapterMode::OnFrame(Timestamp post_time,
|
||||||
|
bool queue_overload,
|
||||||
|
const VideoFrame& frame) {
|
||||||
|
// We expect `metronome_` and `EncodeAllEnqueuedFrames()` runs on
|
||||||
|
// `worker_queue_`.
|
||||||
|
if (!worker_queue_->IsCurrent()) {
|
||||||
|
worker_queue_->PostTask(SafeTask(
|
||||||
|
worker_safety_.flag(), [this, post_time, queue_overload, frame] {
|
||||||
|
OnFrame(post_time, queue_overload, frame);
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RTC_DCHECK_RUN_ON(&worker_sequence_checker_);
|
||||||
|
TRACE_EVENT0("webrtc", "VSyncEncodeAdapterMode::OnFrame");
|
||||||
|
|
||||||
|
input_queue_.emplace_back(std::move(frame), post_time);
|
||||||
|
|
||||||
|
// The `metronome_` tick period maybe throttled in some case, so here we only
|
||||||
|
// align encode task to VSync event when `metronome_` tick period is less
|
||||||
|
// than 34ms (30Hz).
|
||||||
|
static constexpr TimeDelta kMaxAllowedDelay = TimeDelta::Millis(34);
|
||||||
|
if (metronome_->TickPeriod() <= kMaxAllowedDelay) {
|
||||||
|
// The metronome is ticking frequently enough that it is worth the extra
|
||||||
|
// delay.
|
||||||
|
metronome_->RequestCallOnNextTick(
|
||||||
|
SafeTask(worker_safety_.flag(), [this] { EncodeAllEnqueuedFrames(); }));
|
||||||
|
} else {
|
||||||
|
// The metronome is ticking too infrequently, encode immediately.
|
||||||
|
EncodeAllEnqueuedFrames();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VSyncEncodeAdapterMode::EncodeAllEnqueuedFrames() {
|
||||||
|
RTC_DCHECK_RUN_ON(&worker_sequence_checker_);
|
||||||
|
TRACE_EVENT0("webrtc", "VSyncEncodeAdapterMode::EncodeAllEnqueuedFrames");
|
||||||
|
|
||||||
|
// Local time in webrtc time base.
|
||||||
|
Timestamp post_time = clock_->CurrentTime();
|
||||||
|
|
||||||
|
for (auto& input : input_queue_) {
|
||||||
|
TRACE_EVENT1("webrtc", "FrameCadenceAdapterImpl::EncodeAllEnqueuedFrames",
|
||||||
|
"VSyncEncodeDelay",
|
||||||
|
(post_time - input.time_when_posted_us).ms());
|
||||||
|
|
||||||
|
const VideoFrame frame = std::move(input.video_frame);
|
||||||
|
queue_->PostTask(SafeTask(queue_safety_flag_, [this, post_time, frame] {
|
||||||
|
RTC_DCHECK_RUN_ON(queue_);
|
||||||
|
|
||||||
|
// TODO(b/304158952): Support more refined queue overload control.
|
||||||
|
callback_->OnFrame(post_time, /*queue_overload=*/false, frame);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
input_queue_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
FrameCadenceAdapterImpl::FrameCadenceAdapterImpl(
|
FrameCadenceAdapterImpl::FrameCadenceAdapterImpl(
|
||||||
Clock* clock,
|
Clock* clock,
|
||||||
TaskQueueBase* queue,
|
TaskQueueBase* queue,
|
||||||
|
Metronome* metronome,
|
||||||
|
TaskQueueBase* worker_queue,
|
||||||
const FieldTrialsView& field_trials)
|
const FieldTrialsView& field_trials)
|
||||||
: clock_(clock),
|
: clock_(clock),
|
||||||
queue_(queue),
|
queue_(queue),
|
||||||
zero_hertz_screenshare_enabled_(
|
zero_hertz_screenshare_enabled_(
|
||||||
!field_trials.IsDisabled("WebRTC-ZeroHertzScreenshare")) {}
|
!field_trials.IsDisabled("WebRTC-ZeroHertzScreenshare")),
|
||||||
|
metronome_(metronome),
|
||||||
|
worker_queue_(worker_queue) {}
|
||||||
|
|
||||||
FrameCadenceAdapterImpl::~FrameCadenceAdapterImpl() {
|
FrameCadenceAdapterImpl::~FrameCadenceAdapterImpl() {
|
||||||
RTC_DLOG(LS_VERBOSE) << __func__ << " this " << this;
|
RTC_DLOG(LS_VERBOSE) << __func__ << " this " << this;
|
||||||
|
|
||||||
|
// VSync adapter needs to be destroyed on worker queue when metronome is
|
||||||
|
// valid.
|
||||||
|
if (metronome_) {
|
||||||
|
absl::Cleanup cleanup = [adapter = std::move(vsync_encode_adapter_)] {};
|
||||||
|
worker_queue_->PostTask([cleanup = std::move(cleanup)] {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FrameCadenceAdapterImpl::Initialize(Callback* callback) {
|
void FrameCadenceAdapterImpl::Initialize(Callback* callback) {
|
||||||
callback_ = callback;
|
callback_ = callback;
|
||||||
passthrough_adapter_.emplace(clock_, callback);
|
// Use VSync encode mode if metronome is valid, otherwise passthrough mode
|
||||||
current_adapter_mode_ = &passthrough_adapter_.value();
|
// would be used.
|
||||||
|
if (metronome_) {
|
||||||
|
vsync_encode_adapter_ = std::make_unique<VSyncEncodeAdapterMode>(
|
||||||
|
clock_, queue_, safety_.flag(), metronome_, worker_queue_, callback_);
|
||||||
|
} else {
|
||||||
|
passthrough_adapter_.emplace(clock_, callback);
|
||||||
|
}
|
||||||
|
ConfigureCurrentAdapterWithoutZeroHertz();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FrameCadenceAdapterImpl::SetZeroHertzModeEnabled(
|
void FrameCadenceAdapterImpl::SetZeroHertzModeEnabled(
|
||||||
|
@ -655,9 +813,16 @@ absl::optional<uint32_t> FrameCadenceAdapterImpl::GetInputFrameRateFps() {
|
||||||
void FrameCadenceAdapterImpl::UpdateFrameRate() {
|
void FrameCadenceAdapterImpl::UpdateFrameRate() {
|
||||||
RTC_DCHECK_RUN_ON(queue_);
|
RTC_DCHECK_RUN_ON(queue_);
|
||||||
// The frame rate need not be updated for the zero-hertz adapter. The
|
// The frame rate need not be updated for the zero-hertz adapter. The
|
||||||
// passthrough adapter however uses it. Always pass frames into the
|
// vsync encode and passthrough adapter however uses it. Always pass frames
|
||||||
// passthrough to keep the estimation alive should there be an adapter switch.
|
// into the vsync encode or passthrough to keep the estimation alive should
|
||||||
passthrough_adapter_->UpdateFrameRate();
|
// there be an adapter switch.
|
||||||
|
if (metronome_) {
|
||||||
|
RTC_CHECK(vsync_encode_adapter_);
|
||||||
|
vsync_encode_adapter_->UpdateFrameRate();
|
||||||
|
} else {
|
||||||
|
RTC_CHECK(passthrough_adapter_);
|
||||||
|
passthrough_adapter_->UpdateFrameRate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FrameCadenceAdapterImpl::UpdateLayerQualityConvergence(
|
void FrameCadenceAdapterImpl::UpdateLayerQualityConvergence(
|
||||||
|
@ -757,6 +922,17 @@ bool FrameCadenceAdapterImpl::IsZeroHertzScreenshareEnabled() const {
|
||||||
zero_hertz_params_.has_value();
|
zero_hertz_params_.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FrameCadenceAdapterImpl::ConfigureCurrentAdapterWithoutZeroHertz() {
|
||||||
|
// Enable VSyncEncodeAdapterMode if metronome is valid.
|
||||||
|
if (metronome_) {
|
||||||
|
RTC_CHECK(vsync_encode_adapter_);
|
||||||
|
current_adapter_mode_ = vsync_encode_adapter_.get();
|
||||||
|
} else {
|
||||||
|
RTC_CHECK(passthrough_adapter_);
|
||||||
|
current_adapter_mode_ = &passthrough_adapter_.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FrameCadenceAdapterImpl::MaybeReconfigureAdapters(
|
void FrameCadenceAdapterImpl::MaybeReconfigureAdapters(
|
||||||
bool was_zero_hertz_enabled) {
|
bool was_zero_hertz_enabled) {
|
||||||
RTC_DCHECK_RUN_ON(queue_);
|
RTC_DCHECK_RUN_ON(queue_);
|
||||||
|
@ -780,7 +956,7 @@ void FrameCadenceAdapterImpl::MaybeReconfigureAdapters(
|
||||||
zero_hertz_adapter_ = absl::nullopt;
|
zero_hertz_adapter_ = absl::nullopt;
|
||||||
RTC_LOG(LS_INFO) << "Zero hertz mode disabled.";
|
RTC_LOG(LS_INFO) << "Zero hertz mode disabled.";
|
||||||
}
|
}
|
||||||
current_adapter_mode_ = &passthrough_adapter_.value();
|
ConfigureCurrentAdapterWithoutZeroHertz();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -789,8 +965,11 @@ void FrameCadenceAdapterImpl::MaybeReconfigureAdapters(
|
||||||
std::unique_ptr<FrameCadenceAdapterInterface>
|
std::unique_ptr<FrameCadenceAdapterInterface>
|
||||||
FrameCadenceAdapterInterface::Create(Clock* clock,
|
FrameCadenceAdapterInterface::Create(Clock* clock,
|
||||||
TaskQueueBase* queue,
|
TaskQueueBase* queue,
|
||||||
|
Metronome* metronome,
|
||||||
|
TaskQueueBase* worker_queue,
|
||||||
const FieldTrialsView& field_trials) {
|
const FieldTrialsView& field_trials) {
|
||||||
return std::make_unique<FrameCadenceAdapterImpl>(clock, queue, field_trials);
|
return std::make_unique<FrameCadenceAdapterImpl>(clock, queue, metronome,
|
||||||
|
worker_queue, field_trials);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace webrtc
|
} // namespace webrtc
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include "absl/base/attributes.h"
|
#include "absl/base/attributes.h"
|
||||||
#include "api/field_trials_view.h"
|
#include "api/field_trials_view.h"
|
||||||
|
#include "api/metronome/metronome.h"
|
||||||
#include "api/task_queue/task_queue_base.h"
|
#include "api/task_queue/task_queue_base.h"
|
||||||
#include "api/units/time_delta.h"
|
#include "api/units/time_delta.h"
|
||||||
#include "api/video/video_frame.h"
|
#include "api/video/video_frame.h"
|
||||||
|
@ -81,6 +82,8 @@ class FrameCadenceAdapterInterface
|
||||||
static std::unique_ptr<FrameCadenceAdapterInterface> Create(
|
static std::unique_ptr<FrameCadenceAdapterInterface> Create(
|
||||||
Clock* clock,
|
Clock* clock,
|
||||||
TaskQueueBase* queue,
|
TaskQueueBase* queue,
|
||||||
|
Metronome* metronome,
|
||||||
|
TaskQueueBase* worker_queue,
|
||||||
const FieldTrialsView& field_trials);
|
const FieldTrialsView& field_trials);
|
||||||
|
|
||||||
// Call before using the rest of the API.
|
// Call before using the rest of the API.
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "absl/functional/any_invocable.h"
|
#include "absl/functional/any_invocable.h"
|
||||||
|
#include "api/metronome/test/fake_metronome.h"
|
||||||
#include "api/task_queue/default_task_queue_factory.h"
|
#include "api/task_queue/default_task_queue_factory.h"
|
||||||
#include "api/task_queue/task_queue_base.h"
|
#include "api/task_queue/task_queue_base.h"
|
||||||
#include "api/task_queue/task_queue_factory.h"
|
#include "api/task_queue/task_queue_factory.h"
|
||||||
|
@ -64,8 +65,9 @@ VideoFrame CreateFrameWithTimestamps(
|
||||||
std::unique_ptr<FrameCadenceAdapterInterface> CreateAdapter(
|
std::unique_ptr<FrameCadenceAdapterInterface> CreateAdapter(
|
||||||
const FieldTrialsView& field_trials,
|
const FieldTrialsView& field_trials,
|
||||||
Clock* clock) {
|
Clock* clock) {
|
||||||
return FrameCadenceAdapterInterface::Create(clock, TaskQueueBase::Current(),
|
return FrameCadenceAdapterInterface::Create(
|
||||||
field_trials);
|
clock, TaskQueueBase::Current(), /*metronome=*/nullptr,
|
||||||
|
/*worker_queue=*/nullptr, field_trials);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockCallback : public FrameCadenceAdapterInterface::Callback {
|
class MockCallback : public FrameCadenceAdapterInterface::Callback {
|
||||||
|
@ -593,7 +595,8 @@ TEST(FrameCadenceAdapterTest, IgnoresDropInducedCallbacksPostDestruction) {
|
||||||
auto queue = time_controller.GetTaskQueueFactory()->CreateTaskQueue(
|
auto queue = time_controller.GetTaskQueueFactory()->CreateTaskQueue(
|
||||||
"queue", TaskQueueFactory::Priority::NORMAL);
|
"queue", TaskQueueFactory::Priority::NORMAL);
|
||||||
auto adapter = FrameCadenceAdapterInterface::Create(
|
auto adapter = FrameCadenceAdapterInterface::Create(
|
||||||
time_controller.GetClock(), queue.get(), enabler);
|
time_controller.GetClock(), queue.get(), /*metronome=*/nullptr,
|
||||||
|
/*worker_queue=*/nullptr, enabler);
|
||||||
queue->PostTask([&adapter, &callback] {
|
queue->PostTask([&adapter, &callback] {
|
||||||
adapter->Initialize(callback.get());
|
adapter->Initialize(callback.get());
|
||||||
adapter->SetZeroHertzModeEnabled(
|
adapter->SetZeroHertzModeEnabled(
|
||||||
|
@ -609,6 +612,82 @@ TEST(FrameCadenceAdapterTest, IgnoresDropInducedCallbacksPostDestruction) {
|
||||||
time_controller.AdvanceTime(3 * TimeDelta::Seconds(1) / kMaxFps);
|
time_controller.AdvanceTime(3 * TimeDelta::Seconds(1) / kMaxFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(FrameCadenceAdapterTest, EncodeFramesAreAlignedWithMetronomeTick) {
|
||||||
|
ZeroHertzFieldTrialEnabler enabler;
|
||||||
|
GlobalSimulatedTimeController time_controller(Timestamp::Zero());
|
||||||
|
// Here the metronome interval is 33ms, because the metronome is not
|
||||||
|
// infrequent then the encode tasks are aligned with the tick period.
|
||||||
|
static constexpr TimeDelta kTickPeriod = TimeDelta::Millis(33);
|
||||||
|
auto queue = time_controller.GetTaskQueueFactory()->CreateTaskQueue(
|
||||||
|
"queue", TaskQueueFactory::Priority::NORMAL);
|
||||||
|
auto worker_queue = time_controller.GetTaskQueueFactory()->CreateTaskQueue(
|
||||||
|
"work_queue", TaskQueueFactory::Priority::NORMAL);
|
||||||
|
static test::FakeMetronome metronome(kTickPeriod);
|
||||||
|
auto adapter = FrameCadenceAdapterInterface::Create(
|
||||||
|
time_controller.GetClock(), queue.get(), &metronome, worker_queue.get(),
|
||||||
|
enabler);
|
||||||
|
MockCallback callback;
|
||||||
|
adapter->Initialize(&callback);
|
||||||
|
auto frame = CreateFrame();
|
||||||
|
|
||||||
|
// `callback->OnFrame()` would not be called if only 32ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(0);
|
||||||
|
adapter->OnFrame(frame);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(32));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
// `callback->OnFrame()` should be called if 33ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(1);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(1));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
// `callback->OnFrame()` would not be called if only 32ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(0);
|
||||||
|
// Send two frame before next tick.
|
||||||
|
adapter->OnFrame(frame);
|
||||||
|
adapter->OnFrame(frame);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(32));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
// `callback->OnFrame()` should be called if 33ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(2);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(1));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
// Change the metronome tick period to 67ms (15Hz).
|
||||||
|
metronome.SetTickPeriod(TimeDelta::Millis(67));
|
||||||
|
// Expect the encode would happen immediately.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(1);
|
||||||
|
adapter->OnFrame(frame);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Zero());
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
// Change the metronome tick period to 16ms (60Hz).
|
||||||
|
metronome.SetTickPeriod(TimeDelta::Millis(16));
|
||||||
|
// Expect the encode would not happen if only 15ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(0);
|
||||||
|
adapter->OnFrame(frame);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(15));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
// `callback->OnFrame()` should be called if 16ms went by after
|
||||||
|
// `adapter->OnFrame()`.
|
||||||
|
EXPECT_CALL(callback, OnFrame(_, false, _)).Times(1);
|
||||||
|
time_controller.AdvanceTime(TimeDelta::Millis(1));
|
||||||
|
Mock::VerifyAndClearExpectations(&callback);
|
||||||
|
|
||||||
|
rtc::Event finalized;
|
||||||
|
queue->PostTask([&] {
|
||||||
|
adapter = nullptr;
|
||||||
|
finalized.Set();
|
||||||
|
});
|
||||||
|
finalized.Wait(rtc::Event::kForever);
|
||||||
|
}
|
||||||
|
|
||||||
class FrameCadenceAdapterSimulcastLayersParamTest
|
class FrameCadenceAdapterSimulcastLayersParamTest
|
||||||
: public ::testing::TestWithParam<int> {
|
: public ::testing::TestWithParam<int> {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -114,6 +114,7 @@ std::unique_ptr<VideoStreamEncoder> CreateVideoStreamEncoder(
|
||||||
VideoStreamEncoder::BitrateAllocationCallbackType
|
VideoStreamEncoder::BitrateAllocationCallbackType
|
||||||
bitrate_allocation_callback_type,
|
bitrate_allocation_callback_type,
|
||||||
const FieldTrialsView& field_trials,
|
const FieldTrialsView& field_trials,
|
||||||
|
Metronome* metronome,
|
||||||
webrtc::VideoEncoderFactory::EncoderSelectorInterface* encoder_selector) {
|
webrtc::VideoEncoderFactory::EncoderSelectorInterface* encoder_selector) {
|
||||||
std::unique_ptr<TaskQueueBase, TaskQueueDeleter> encoder_queue =
|
std::unique_ptr<TaskQueueBase, TaskQueueDeleter> encoder_queue =
|
||||||
task_queue_factory->CreateTaskQueue("EncoderQueue",
|
task_queue_factory->CreateTaskQueue("EncoderQueue",
|
||||||
|
@ -122,8 +123,9 @@ std::unique_ptr<VideoStreamEncoder> CreateVideoStreamEncoder(
|
||||||
return std::make_unique<VideoStreamEncoder>(
|
return std::make_unique<VideoStreamEncoder>(
|
||||||
clock, num_cpu_cores, stats_proxy, encoder_settings,
|
clock, num_cpu_cores, stats_proxy, encoder_settings,
|
||||||
std::make_unique<OveruseFrameDetector>(stats_proxy),
|
std::make_unique<OveruseFrameDetector>(stats_proxy),
|
||||||
FrameCadenceAdapterInterface::Create(clock, encoder_queue_ptr,
|
FrameCadenceAdapterInterface::Create(
|
||||||
field_trials),
|
clock, encoder_queue_ptr, metronome,
|
||||||
|
/*worker_queue=*/TaskQueueBase::Current(), field_trials),
|
||||||
std::move(encoder_queue), bitrate_allocation_callback_type, field_trials,
|
std::move(encoder_queue), bitrate_allocation_callback_type, field_trials,
|
||||||
encoder_selector);
|
encoder_selector);
|
||||||
}
|
}
|
||||||
|
@ -139,6 +141,7 @@ VideoSendStream::VideoSendStream(
|
||||||
TaskQueueBase* network_queue,
|
TaskQueueBase* network_queue,
|
||||||
RtcpRttStats* call_stats,
|
RtcpRttStats* call_stats,
|
||||||
RtpTransportControllerSendInterface* transport,
|
RtpTransportControllerSendInterface* transport,
|
||||||
|
Metronome* metronome,
|
||||||
BitrateAllocatorInterface* bitrate_allocator,
|
BitrateAllocatorInterface* bitrate_allocator,
|
||||||
SendDelayStats* send_delay_stats,
|
SendDelayStats* send_delay_stats,
|
||||||
RtcEventLog* event_log,
|
RtcEventLog* event_log,
|
||||||
|
@ -161,6 +164,7 @@ VideoSendStream::VideoSendStream(
|
||||||
config_.encoder_settings,
|
config_.encoder_settings,
|
||||||
GetBitrateAllocationCallbackType(config_, field_trials),
|
GetBitrateAllocationCallbackType(config_, field_trials),
|
||||||
field_trials,
|
field_trials,
|
||||||
|
metronome,
|
||||||
config_.encoder_selector)),
|
config_.encoder_selector)),
|
||||||
encoder_feedback_(
|
encoder_feedback_(
|
||||||
clock,
|
clock,
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
#include "api/fec_controller.h"
|
#include "api/fec_controller.h"
|
||||||
#include "api/field_trials_view.h"
|
#include "api/field_trials_view.h"
|
||||||
|
#include "api/metronome/metronome.h"
|
||||||
#include "api/sequence_checker.h"
|
#include "api/sequence_checker.h"
|
||||||
#include "api/task_queue/pending_task_safety_flag.h"
|
#include "api/task_queue/pending_task_safety_flag.h"
|
||||||
#include "call/bitrate_allocator.h"
|
#include "call/bitrate_allocator.h"
|
||||||
|
@ -62,6 +63,7 @@ class VideoSendStream : public webrtc::VideoSendStream {
|
||||||
TaskQueueBase* network_queue,
|
TaskQueueBase* network_queue,
|
||||||
RtcpRttStats* call_stats,
|
RtcpRttStats* call_stats,
|
||||||
RtpTransportControllerSendInterface* transport,
|
RtpTransportControllerSendInterface* transport,
|
||||||
|
Metronome* metronome,
|
||||||
BitrateAllocatorInterface* bitrate_allocator,
|
BitrateAllocatorInterface* bitrate_allocator,
|
||||||
SendDelayStats* send_delay_stats,
|
SendDelayStats* send_delay_stats,
|
||||||
RtcEventLog* event_log,
|
RtcEventLog* event_log,
|
||||||
|
|
|
@ -875,8 +875,9 @@ class VideoStreamEncoderTest : public ::testing::Test {
|
||||||
"EncoderQueue", TaskQueueFactory::Priority::NORMAL);
|
"EncoderQueue", TaskQueueFactory::Priority::NORMAL);
|
||||||
TaskQueueBase* encoder_queue_ptr = encoder_queue.get();
|
TaskQueueBase* encoder_queue_ptr = encoder_queue.get();
|
||||||
std::unique_ptr<FrameCadenceAdapterInterface> cadence_adapter =
|
std::unique_ptr<FrameCadenceAdapterInterface> cadence_adapter =
|
||||||
FrameCadenceAdapterInterface::Create(time_controller_.GetClock(),
|
FrameCadenceAdapterInterface::Create(
|
||||||
encoder_queue_ptr, field_trials_);
|
time_controller_.GetClock(), encoder_queue_ptr,
|
||||||
|
/*metronome=*/nullptr, /*worker_queue=*/nullptr, field_trials_);
|
||||||
video_stream_encoder_ = std::make_unique<VideoStreamEncoderUnderTest>(
|
video_stream_encoder_ = std::make_unique<VideoStreamEncoderUnderTest>(
|
||||||
&time_controller_, std::move(cadence_adapter), std::move(encoder_queue),
|
&time_controller_, std::move(cadence_adapter), std::move(encoder_queue),
|
||||||
stats_proxy_.get(), video_send_config_.encoder_settings,
|
stats_proxy_.get(), video_send_config_.encoder_settings,
|
||||||
|
@ -9556,7 +9557,7 @@ TEST(VideoStreamEncoderFrameCadenceTest,
|
||||||
"WebRTC-ZeroHertzScreenshare/Enabled/");
|
"WebRTC-ZeroHertzScreenshare/Enabled/");
|
||||||
auto adapter = FrameCadenceAdapterInterface::Create(
|
auto adapter = FrameCadenceAdapterInterface::Create(
|
||||||
factory.GetTimeController()->GetClock(), encoder_queue.get(),
|
factory.GetTimeController()->GetClock(), encoder_queue.get(),
|
||||||
field_trials);
|
/*metronome=*/nullptr, /*worker_queue=*/nullptr, field_trials);
|
||||||
FrameCadenceAdapterInterface* adapter_ptr = adapter.get();
|
FrameCadenceAdapterInterface* adapter_ptr = adapter.get();
|
||||||
|
|
||||||
MockVideoSourceInterface mock_source;
|
MockVideoSourceInterface mock_source;
|
||||||
|
|
Loading…
Reference in a new issue