diff --git a/call/video_receive_stream.h b/call/video_receive_stream.h index 395a3d2673..b5c1649aa9 100644 --- a/call/video_receive_stream.h +++ b/call/video_receive_stream.h @@ -19,6 +19,7 @@ #include "api/call/transport.h" #include "api/rtp_headers.h" #include "api/rtpparameters.h" +#include "api/rtpreceiverinterface.h" #include "api/video/video_content_type.h" #include "api/video/video_sink_interface.h" #include "api/video/video_timing.h" @@ -239,6 +240,8 @@ class VideoReceiveStream { virtual void AddSecondarySink(RtpPacketSinkInterface* sink) = 0; virtual void RemoveSecondarySink(const RtpPacketSinkInterface* sink) = 0; + virtual std::vector GetSources() const = 0; + protected: virtual ~VideoReceiveStream() {} }; diff --git a/media/BUILD.gn b/media/BUILD.gn index 241856f48e..8b6de8dcfb 100644 --- a/media/BUILD.gn +++ b/media/BUILD.gn @@ -487,6 +487,7 @@ if (rtc_include_tests) { ":rtc_audio_video", ":rtc_constants", ":rtc_data", + "../api/units:time_delta", "../api/video:video_frame_i420", "../modules/audio_processing:mocks", "../modules/rtp_rtcp", diff --git a/media/base/fakemediaengine.h b/media/base/fakemediaengine.h index 5d381f7b41..cdea187bf5 100644 --- a/media/base/fakemediaengine.h +++ b/media/base/fakemediaengine.h @@ -601,6 +601,10 @@ class FakeVideoMediaChannel : public RtpHelper { void FillBitrateInfo(BandwidthEstimationInfo* bwe_info) override {} bool GetStats(VideoMediaInfo* info) override { return false; } + std::vector GetSources(uint32_t ssrc) const override { + return {}; + } + private: bool SetRecvCodecs(const std::vector& codecs) { if (fail_set_recv_codecs()) { diff --git a/media/base/mediachannel.h b/media/base/mediachannel.h index c7d8580dbc..44beb8fcf2 100644 --- a/media/base/mediachannel.h +++ b/media/base/mediachannel.h @@ -784,6 +784,8 @@ class VideoMediaChannel : public MediaChannel { virtual void FillBitrateInfo(BandwidthEstimationInfo* bwe_info) = 0; // Gets quality stats for the channel. virtual bool GetStats(VideoMediaInfo* info) = 0; + + virtual std::vector GetSources(uint32_t ssrc) const = 0; }; enum DataMessageType { diff --git a/media/engine/fakewebrtccall.h b/media/engine/fakewebrtccall.h index 8420f864ab..5bd335b949 100644 --- a/media/engine/fakewebrtccall.h +++ b/media/engine/fakewebrtccall.h @@ -210,6 +210,10 @@ class FakeVideoReceiveStream final : public webrtc::VideoReceiveStream { int GetNumAddedSecondarySinks() const; int GetNumRemovedSecondarySinks() const; + std::vector GetSources() const override { + return std::vector(); + } + private: // webrtc::VideoReceiveStream implementation. void Start() override; diff --git a/media/engine/webrtcvideoengine.cc b/media/engine/webrtcvideoengine.cc index 29b0eb9194..501f5de89d 100644 --- a/media/engine/webrtcvideoengine.cc +++ b/media/engine/webrtcvideoengine.cc @@ -1480,6 +1480,20 @@ absl::optional WebRtcVideoChannel::GetDefaultReceiveStreamSsrc() { return ssrc; } +std::vector WebRtcVideoChannel::GetSources( + uint32_t ssrc) const { + rtc::CritScope stream_lock(&stream_crit_); + auto it = receive_streams_.find(ssrc); + if (it == receive_streams_.end()) { + // TODO(bugs.webrtc.org/9781): Investigate standard compliance + // with sources for streams that has been removed. + RTC_LOG(LS_ERROR) << "Attempting to get contributing sources for SSRC:" + << ssrc << " which doesn't exist."; + return {}; + } + return it->second->GetSources(); +} + bool WebRtcVideoChannel::SendRtp(const uint8_t* data, size_t len, const webrtc::PacketOptions& options) { @@ -2202,6 +2216,12 @@ WebRtcVideoChannel::WebRtcVideoReceiveStream::GetSsrcs() const { return stream_params_.ssrcs; } +std::vector +WebRtcVideoChannel::WebRtcVideoReceiveStream::GetSources() { + RTC_DCHECK(stream_); + return stream_->GetSources(); +} + absl::optional WebRtcVideoChannel::WebRtcVideoReceiveStream::GetFirstPrimarySsrc() const { std::vector primary_ssrcs; diff --git a/media/engine/webrtcvideoengine.h b/media/engine/webrtcvideoengine.h index 49ac1002bd..d949d786b7 100644 --- a/media/engine/webrtcvideoengine.h +++ b/media/engine/webrtcvideoengine.h @@ -175,6 +175,8 @@ class WebRtcVideoChannel : public VideoMediaChannel, public webrtc::Transport { static constexpr int kDefaultQpMax = 56; + std::vector GetSources(uint32_t ssrc) const override; + private: class WebRtcVideoReceiveStream; struct VideoCodecSettings { @@ -357,6 +359,8 @@ class WebRtcVideoChannel : public VideoMediaChannel, public webrtc::Transport { const std::vector& GetSsrcs() const; + std::vector GetSources(); + // Does not return codecs, they are filled by the owning WebRtcVideoChannel. webrtc::RtpParameters GetRtpParameters() const; diff --git a/media/engine/webrtcvideoengine_unittest.cc b/media/engine/webrtcvideoengine_unittest.cc index a5a4f98a74..af4413871c 100644 --- a/media/engine/webrtcvideoengine_unittest.cc +++ b/media/engine/webrtcvideoengine_unittest.cc @@ -17,6 +17,7 @@ #include "api/rtpparameters.h" #include "api/test/mock_video_decoder_factory.h" #include "api/test/mock_video_encoder_factory.h" +#include "api/units/time_delta.h" #include "api/video_codecs/builtin_video_decoder_factory.h" #include "api/video_codecs/builtin_video_encoder_factory.h" #include "api/video_codecs/sdp_video_format.h" @@ -965,6 +966,25 @@ TEST_F(WebRtcVideoEngineTest, RegisterH264DecoderIfSupported) { ASSERT_EQ(1u, decoder_factory_->decoders().size()); } +// Tests when GetSources is called with non-existing ssrc, it will return an +// empty list of RtpSource without crashing. +TEST_F(WebRtcVideoEngineTest, GetSourcesWithNonExistingSsrc) { + // Setup an recv stream with |kSsrc|. + encoder_factory_->AddSupportedVideoCodecType("VP8"); + decoder_factory_->AddSupportedVideoCodecType(webrtc::SdpVideoFormat("VP8")); + cricket::VideoRecvParameters parameters; + parameters.codecs.push_back(GetEngineCodec("VP8")); + std::unique_ptr channel( + SetRecvParamsWithSupportedCodecs(parameters.codecs)); + + EXPECT_TRUE( + channel->AddRecvStream(cricket::StreamParams::CreateLegacy(kSsrc))); + + // Call GetSources with |kSsrc + 1| which doesn't exist. + std::vector sources = channel->GetSources(kSsrc + 1); + EXPECT_EQ(0u, sources.size()); +} + TEST(WebRtcVideoEngineNewVideoCodecFactoryTest, NullFactories) { std::unique_ptr encoder_factory; std::unique_ptr decoder_factory; @@ -1201,11 +1221,14 @@ TEST_F(WebRtcVideoEngineTest, DISABLED_RecreatesEncoderOnContentTypeChange) { class WebRtcVideoChannelBaseTest : public testing::Test { protected: WebRtcVideoChannelBaseTest() - : call_(webrtc::Call::Create(webrtc::Call::Config(&event_log_))), - engine_(webrtc::CreateBuiltinVideoEncoderFactory(), + : engine_(webrtc::CreateBuiltinVideoEncoderFactory(), webrtc::CreateBuiltinVideoDecoderFactory()) {} virtual void SetUp() { + // One testcase calls SetUp in a loop, only create call_ once. + if (!call_) { + call_.reset(webrtc::Call::Create(webrtc::Call::Config(&event_log_))); + } cricket::MediaConfig media_config; // Disabling cpu overuse detection actually disables quality scaling too; it // implies DegradationPreference kMaintainResolution. Automatic scaling @@ -1402,7 +1425,7 @@ class WebRtcVideoChannelBaseTest : public testing::Test { } webrtc::RtcEventLogNullImpl event_log_; - const std::unique_ptr call_; + std::unique_ptr call_; WebRtcVideoEngine engine_; std::unique_ptr video_capturer_; std::unique_ptr video_capturer_2_; @@ -6696,4 +6719,145 @@ TEST_F(WebRtcVideoChannelSimulcastTest, false); } +// The fake clock needs to be initialize before the call. +// So defer creating call in base class. +class WebRtcVideoChannelTestWithClock : public WebRtcVideoChannelBaseTest { + public: + WebRtcVideoChannelTestWithClock() { + fake_clock_.AdvanceTime(webrtc::TimeDelta::ms(1)); // avoid time=0 + } + rtc::ScopedFakeClock fake_clock_; +}; + +TEST_F(WebRtcVideoChannelTestWithClock, GetSources) { + uint8_t data1[] = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + EXPECT_EQ(0u, channel_->GetSources(kSsrc).size()); + + rtc::CopyOnWriteBuffer packet1(data1, sizeof(data1)); + rtc::SetBE32(packet1.data() + 8, kSsrc); + channel_->SetSink(kDefaultReceiveSsrc, NULL); + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + channel_->OnPacketReceived(&packet1, rtc::PacketTime()); + + std::vector sources = channel_->GetSources(kSsrc); + EXPECT_EQ(1u, sources.size()); + EXPECT_EQ(webrtc::RtpSourceType::SSRC, sources[0].source_type()); + int64_t timestamp1 = sources[0].timestamp_ms(); + + // a new packet. + int64_t timeDeltaMs = 1; + fake_clock_.AdvanceTime(webrtc::TimeDelta::ms(timeDeltaMs)); + channel_->OnPacketReceived(&packet1, rtc::PacketTime()); + int64_t timestamp2 = channel_->GetSources(kSsrc)[0].timestamp_ms(); + EXPECT_EQ(timestamp2, timestamp1 + timeDeltaMs); + + // It only keeps 10s of history. + fake_clock_.AdvanceTime(webrtc::TimeDelta::seconds(10)); + fake_clock_.AdvanceTime(webrtc::TimeDelta::ms(1)); + EXPECT_EQ(0u, channel_->GetSources(kSsrc).size()); +} + +TEST_F(WebRtcVideoChannelTestWithClock, GetContributingSources) { + uint8_t data1[] = {0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + uint32_t kCsrc = 4321u; + EXPECT_EQ(0u, channel_->GetSources(kSsrc).size()); + EXPECT_EQ(0u, channel_->GetSources(kCsrc).size()); + + rtc::CopyOnWriteBuffer packet1(data1, sizeof(data1)); + rtc::SetBE32(packet1.data() + 8, kSsrc); + rtc::SetBE32(packet1.data() + 12, kCsrc); + channel_->SetSink(kDefaultReceiveSsrc, NULL); + EXPECT_TRUE(SetDefaultCodec()); + EXPECT_TRUE(SetSend(true)); + EXPECT_EQ(0, renderer_.num_rendered_frames()); + channel_->OnPacketReceived(&packet1, rtc::PacketTime()); + + { + ASSERT_EQ(2u, channel_->GetSources(kSsrc).size()); + EXPECT_EQ(0u, channel_->GetSources(kCsrc).size()); + std::vector sources = channel_->GetSources(kSsrc); + EXPECT_EQ(sources[0].timestamp_ms(), sources[1].timestamp_ms()); + // 1 SSRC and 1 CSRC. + EXPECT_EQ(1, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::SSRC; + })); + EXPECT_EQ(1, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::CSRC; + })); + } + int64_t timestamp1 = channel_->GetSources(kSsrc)[0].timestamp_ms(); + + // a new packet with only ssrc (i.e no csrc). + uint8_t data2[] = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + rtc::CopyOnWriteBuffer packet2(data2, sizeof(data2)); + rtc::SetBE32(packet2.data() + 8, kSsrc); + + int64_t timeDeltaMs = 1; + fake_clock_.AdvanceTime(webrtc::TimeDelta::ms(timeDeltaMs)); + channel_->OnPacketReceived(&packet2, rtc::PacketTime()); + + { + ASSERT_EQ(2u, channel_->GetSources(kSsrc).size()); + EXPECT_EQ(0u, channel_->GetSources(kCsrc).size()); + std::vector sources = channel_->GetSources(kSsrc); + EXPECT_NE(sources[0].timestamp_ms(), sources[1].timestamp_ms()); + // 1 SSRC and 1 CSRC. + EXPECT_EQ(1, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::SSRC; + })); + EXPECT_EQ(1, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::CSRC; + })); + auto ssrcSource = std::find_if( + sources.begin(), sources.end(), [](const webrtc::RtpSource& source) { + return source.source_type() == webrtc::RtpSourceType::SSRC; + }); + auto csrcSource = std::find_if( + sources.begin(), sources.end(), [](const webrtc::RtpSource& source) { + return source.source_type() == webrtc::RtpSourceType::CSRC; + }); + + EXPECT_EQ(ssrcSource->timestamp_ms(), timestamp1 + timeDeltaMs); + EXPECT_EQ(csrcSource->timestamp_ms(), timestamp1); + } + + // It only keeps 10s of history. + fake_clock_.AdvanceTime(webrtc::TimeDelta::seconds(10)); + + { + ASSERT_EQ(1u, channel_->GetSources(kSsrc).size()); + EXPECT_EQ(0u, channel_->GetSources(kCsrc).size()); + std::vector sources = channel_->GetSources(kSsrc); + EXPECT_EQ(1, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::SSRC; + })); + EXPECT_EQ(0, std::count_if(sources.begin(), sources.end(), + [](const webrtc::RtpSource& source) { + return source.source_type() == + webrtc::RtpSourceType::CSRC; + })); + } + + fake_clock_.AdvanceTime(webrtc::TimeDelta::ms(1)); + EXPECT_EQ(0u, channel_->GetSources(kSsrc).size()); + EXPECT_EQ(0u, channel_->GetSources(kCsrc).size()); +} + } // namespace cricket diff --git a/pc/peerconnection_integrationtest.cc b/pc/peerconnection_integrationtest.cc index dd977b4d3f..3cf3e58407 100644 --- a/pc/peerconnection_integrationtest.cc +++ b/pc/peerconnection_integrationtest.cc @@ -4308,7 +4308,7 @@ TEST_P(PeerConnectionIntegrationTest, CodecNamesAreCaseInsensitive) { ASSERT_TRUE(ExpectNewFrames(media_expectations)); } -TEST_P(PeerConnectionIntegrationTest, GetSources) { +TEST_P(PeerConnectionIntegrationTest, GetSourcesAudio) { ASSERT_TRUE(CreatePeerConnectionWrappers()); ConnectFakeSignaling(); caller()->AddAudioTrack(); @@ -4318,14 +4318,34 @@ TEST_P(PeerConnectionIntegrationTest, GetSources) { MediaExpectations media_expectations; media_expectations.CalleeExpectsSomeAudio(1); ASSERT_TRUE(ExpectNewFrames(media_expectations)); - ASSERT_GT(callee()->pc()->GetReceivers().size(), 0u); + ASSERT_EQ(callee()->pc()->GetReceivers().size(), 1u); auto receiver = callee()->pc()->GetReceivers()[0]; ASSERT_EQ(receiver->media_type(), cricket::MEDIA_TYPE_AUDIO); - - auto contributing_sources = receiver->GetSources(); + auto sources = receiver->GetSources(); ASSERT_GT(receiver->GetParameters().encodings.size(), 0u); EXPECT_EQ(receiver->GetParameters().encodings[0].ssrc, - contributing_sources[0].source_id()); + sources[0].source_id()); + EXPECT_EQ(webrtc::RtpSourceType::SSRC, sources[0].source_type()); +} + +TEST_P(PeerConnectionIntegrationTest, GetSourcesVideo) { + ASSERT_TRUE(CreatePeerConnectionWrappers()); + ConnectFakeSignaling(); + caller()->AddVideoTrack(); + caller()->CreateAndSetAndSignalOffer(); + ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout); + // Wait for one video frame to be received by the callee. + MediaExpectations media_expectations; + media_expectations.CalleeExpectsSomeVideo(1); + ASSERT_TRUE(ExpectNewFrames(media_expectations)); + ASSERT_EQ(callee()->pc()->GetReceivers().size(), 1u); + auto receiver = callee()->pc()->GetReceivers()[0]; + ASSERT_EQ(receiver->media_type(), cricket::MEDIA_TYPE_VIDEO); + auto sources = receiver->GetSources(); + ASSERT_GT(receiver->GetParameters().encodings.size(), 0u); + EXPECT_EQ(receiver->GetParameters().encodings[0].ssrc, + sources[0].source_id()); + EXPECT_EQ(webrtc::RtpSourceType::SSRC, sources[0].source_type()); } // Test that if a track is removed and added again with a different stream ID, diff --git a/pc/rtpreceiver.cc b/pc/rtpreceiver.cc index ae31f843d1..8ab08bf8f2 100644 --- a/pc/rtpreceiver.cc +++ b/pc/rtpreceiver.cc @@ -440,4 +440,12 @@ void VideoRtpReceiver::NotifyFirstPacketReceived() { received_first_packet_ = true; } +std::vector VideoRtpReceiver::GetSources() const { + if (!media_channel_ || !ssrc_ || stopped_) { + return {}; + } + return worker_thread_->Invoke>( + RTC_FROM_HERE, [&] { return media_channel_->GetSources(*ssrc_); }); +} + } // namespace webrtc diff --git a/pc/rtpreceiver.h b/pc/rtpreceiver.h index 7f77d85bcd..85be8c258b 100644 --- a/pc/rtpreceiver.h +++ b/pc/rtpreceiver.h @@ -227,6 +227,8 @@ class VideoRtpReceiver : public rtc::RefCountedObject { int AttachmentId() const override { return attachment_id_; } + std::vector GetSources() const override; + private: class VideoRtpTrackSource : public VideoTrackSource { public: diff --git a/video/rtp_video_stream_receiver.cc b/video/rtp_video_stream_receiver.cc index 5939b524df..ecd62ae082 100644 --- a/video/rtp_video_stream_receiver.cc +++ b/video/rtp_video_stream_receiver.cc @@ -185,7 +185,7 @@ absl::optional RtpVideoStreamReceiver::GetSyncInfo() const { return absl::nullopt; } { - rtc::CritScope lock(&last_seq_num_cs_); + rtc::CritScope lock(&rtp_sources_lock_); if (!last_received_rtp_timestamp_ || !last_received_rtp_system_time_ms_) { return absl::nullopt; } @@ -283,13 +283,15 @@ void RtpVideoStreamReceiver::OnRtpPacket(const RtpPacketReceived& packet) { } if (!packet.recovered()) { + // TODO(nisse): Exclude out-of-order packets? int64_t now_ms = clock_->TimeInMilliseconds(); { - rtc::CritScope lock(&last_seq_num_cs_); - - // TODO(nisse): Exclude out-of-order packets? + rtc::CritScope cs(&rtp_sources_lock_); last_received_rtp_timestamp_ = packet.Timestamp(); last_received_rtp_system_time_ms_ = now_ms; + + std::vector csrcs = packet.Csrcs(); + contributing_sources_.Update(now_ms, csrcs); } // Periodically log the RTP header of incoming packets. if (now_ms - last_packet_log_ms_ > kPacketLogIntervalMs) { @@ -671,4 +673,19 @@ void RtpVideoStreamReceiver::InsertSpsPpsIntoTracker(uint8_t payload_type) { sprop_decoder.pps_nalu()); } +std::vector RtpVideoStreamReceiver::GetSources() const { + int64_t now_ms = rtc::TimeMillis(); + std::vector sources; + { + rtc::CritScope cs(&rtp_sources_lock_); + sources = contributing_sources_.GetSources(now_ms); + if (last_received_rtp_system_time_ms_ >= + now_ms - ContributingSources::kHistoryMs) { + sources.emplace_back(*last_received_rtp_system_time_ms_, + config_.rtp.remote_ssrc, RtpSourceType::SSRC); + } + } + return sources; +} + } // namespace webrtc diff --git a/video/rtp_video_stream_receiver.h b/video/rtp_video_stream_receiver.h index e67d66389e..54398871a8 100644 --- a/video/rtp_video_stream_receiver.h +++ b/video/rtp_video_stream_receiver.h @@ -29,6 +29,7 @@ #include "modules/rtp_rtcp/include/rtp_header_extension_map.h" #include "modules/rtp_rtcp/include/rtp_rtcp.h" #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" +#include "modules/rtp_rtcp/source/contributing_sources.h" #include "modules/video_coding/h264_sps_pps_tracker.h" #include "modules/video_coding/include/video_coding_defines.h" #include "modules/video_coding/packet_buffer.h" @@ -134,6 +135,8 @@ class RtpVideoStreamReceiver : public RtpData, void AddSecondarySink(RtpPacketSinkInterface* sink); void RemoveSecondarySink(const RtpPacketSinkInterface* sink); + std::vector GetSources() const; + private: // Entry point doing non-stats work for a received packet. Called // for the same packet both before and after RED decapsulation. @@ -177,10 +180,6 @@ class RtpVideoStreamReceiver : public RtpData, RTC_GUARDED_BY(last_seq_num_cs_); video_coding::H264SpsPpsTracker tracker_; - absl::optional last_received_rtp_timestamp_ - RTC_GUARDED_BY(last_seq_num_cs_); - absl::optional last_received_rtp_system_time_ms_ - RTC_GUARDED_BY(last_seq_num_cs_); std::map pt_codec_type_; // TODO(johan): Remove pt_codec_params_ once // https://bugs.chromium.org/p/webrtc/issues/detail?id=6883 is resolved. @@ -192,6 +191,15 @@ class RtpVideoStreamReceiver : public RtpData, std::vector secondary_sinks_ RTC_GUARDED_BY(worker_task_checker_); + + // Info for GetSources and GetSyncInfo is updated on network or worker thread, + // queried on the worker thread. + rtc::CriticalSection rtp_sources_lock_; + ContributingSources contributing_sources_ RTC_GUARDED_BY(&rtp_sources_lock_); + absl::optional last_received_rtp_timestamp_ + RTC_GUARDED_BY(rtp_sources_lock_); + absl::optional last_received_rtp_system_time_ms_ + RTC_GUARDED_BY(rtp_sources_lock_); }; } // namespace webrtc diff --git a/video/video_receive_stream.cc b/video/video_receive_stream.cc index b8f28963ea..7facf85dda 100644 --- a/video/video_receive_stream.cc +++ b/video/video_receive_stream.cc @@ -459,5 +459,10 @@ bool VideoReceiveStream::Decode() { } return true; } + +std::vector VideoReceiveStream::GetSources() const { + return rtp_video_stream_receiver_.GetSources(); +} + } // namespace internal } // namespace webrtc diff --git a/video/video_receive_stream.h b/video/video_receive_stream.h index 5b1289443b..76ee2ed0fd 100644 --- a/video/video_receive_stream.h +++ b/video/video_receive_stream.h @@ -112,6 +112,8 @@ class VideoReceiveStream : public webrtc::VideoReceiveStream, uint32_t GetPlayoutTimestamp() const override; void SetMinimumPlayoutDelay(int delay_ms) override; + std::vector GetSources() const override; + private: static void DecodeThreadFunction(void* ptr); bool Decode();