For VP9 assume max number of spatial layers to simulate generic descriptor
VP9 allows to increase number of spatial layers on delta frame, which is not supported by dependency descriptor. Thus to generate DD compatible generic header, simulator would set max number of spatial layers, while number of active spatial layers would be communicated with active_decode_target bitmask Bug: webrtc:14042 Change-Id: I4da63fa7c38b0f17758a7a6243640f444470b40c Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/265164 Commit-Queue: Danil Chapovalov <danilchap@webrtc.org> Reviewed-by: Philip Eliasson <philipel@webrtc.org> Cr-Commit-Position: refs/heads/main@{#37151}
This commit is contained in:
parent
6545516a14
commit
5b298ab9dd
@ -30,8 +30,10 @@
|
|||||||
#include "rtc_base/time_utils.h"
|
#include "rtc_base/time_utils.h"
|
||||||
|
|
||||||
namespace webrtc {
|
namespace webrtc {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kMaxSimulatedSpatialLayers = 3;
|
||||||
|
|
||||||
void PopulateRtpWithCodecSpecifics(const CodecSpecificInfo& info,
|
void PopulateRtpWithCodecSpecifics(const CodecSpecificInfo& info,
|
||||||
absl::optional<int> spatial_index,
|
absl::optional<int> spatial_index,
|
||||||
RTPVideoHeader* rtp) {
|
RTPVideoHeader* rtp) {
|
||||||
@ -123,6 +125,50 @@ void SetVideoTiming(const EncodedImage& image, VideoSendTiming* timing) {
|
|||||||
timing->network2_timestamp_delta_ms = 0;
|
timing->network2_timestamp_delta_ms = 0;
|
||||||
timing->flags = image.timing_.flags;
|
timing->flags = image.timing_.flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns structure that aligns with simulated generic info. The templates
|
||||||
|
// allow to produce valid dependency descriptor for any stream where
|
||||||
|
// `num_spatial_layers` * `num_temporal_layers` <= 32 (limited by
|
||||||
|
// https://aomediacodec.github.io/av1-rtp-spec/#a82-syntax, see
|
||||||
|
// template_fdiffs()). The set of the templates is not tuned for any paricular
|
||||||
|
// structure thus dependency descriptor would use more bytes on the wire than
|
||||||
|
// with tuned templates.
|
||||||
|
FrameDependencyStructure MinimalisticStructure(int num_spatial_layers,
|
||||||
|
int num_temporal_layers) {
|
||||||
|
RTC_DCHECK_LE(num_spatial_layers, DependencyDescriptor::kMaxSpatialIds);
|
||||||
|
RTC_DCHECK_LE(num_temporal_layers, DependencyDescriptor::kMaxTemporalIds);
|
||||||
|
RTC_DCHECK_LE(num_spatial_layers * num_temporal_layers, 32);
|
||||||
|
FrameDependencyStructure structure;
|
||||||
|
structure.num_decode_targets = num_spatial_layers * num_temporal_layers;
|
||||||
|
structure.num_chains = num_spatial_layers;
|
||||||
|
structure.templates.reserve(num_spatial_layers * num_temporal_layers);
|
||||||
|
for (int sid = 0; sid < num_spatial_layers; ++sid) {
|
||||||
|
for (int tid = 0; tid < num_temporal_layers; ++tid) {
|
||||||
|
FrameDependencyTemplate a_template;
|
||||||
|
a_template.spatial_id = sid;
|
||||||
|
a_template.temporal_id = tid;
|
||||||
|
for (int s = 0; s < num_spatial_layers; ++s) {
|
||||||
|
for (int t = 0; t < num_temporal_layers; ++t) {
|
||||||
|
// Prefer kSwitch indication for frames that is part of the decode
|
||||||
|
// target because dependency descriptor information generated in this
|
||||||
|
// class use kSwitch indications more often that kRequired, increasing
|
||||||
|
// the chance of a good (or complete) template match.
|
||||||
|
a_template.decode_target_indications.push_back(
|
||||||
|
sid <= s && tid <= t ? DecodeTargetIndication::kSwitch
|
||||||
|
: DecodeTargetIndication::kNotPresent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a_template.frame_diffs.push_back(tid == 0 ? num_spatial_layers *
|
||||||
|
num_temporal_layers
|
||||||
|
: num_spatial_layers);
|
||||||
|
a_template.chain_diffs.assign(structure.num_chains, 1);
|
||||||
|
structure.templates.push_back(a_template);
|
||||||
|
|
||||||
|
structure.decode_target_protected_by_chain.push_back(sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return structure;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
RtpPayloadParams::RtpPayloadParams(const uint32_t ssrc,
|
RtpPayloadParams::RtpPayloadParams(const uint32_t ssrc,
|
||||||
@ -131,7 +177,10 @@ RtpPayloadParams::RtpPayloadParams(const uint32_t ssrc,
|
|||||||
: ssrc_(ssrc),
|
: ssrc_(ssrc),
|
||||||
generic_picture_id_experiment_(
|
generic_picture_id_experiment_(
|
||||||
absl::StartsWith(trials.Lookup("WebRTC-GenericPictureId"),
|
absl::StartsWith(trials.Lookup("WebRTC-GenericPictureId"),
|
||||||
"Enabled")) {
|
"Enabled")),
|
||||||
|
simulate_generic_structure_(absl::StartsWith(
|
||||||
|
trials.Lookup("WebRTC-GenericCodecDependencyDescriptor"),
|
||||||
|
"Enabled")) {
|
||||||
for (auto& spatial_layer : last_shared_frame_id_)
|
for (auto& spatial_layer : last_shared_frame_id_)
|
||||||
spatial_layer.fill(-1);
|
spatial_layer.fill(-1);
|
||||||
|
|
||||||
@ -298,6 +347,69 @@ void RtpPayloadParams::SetGeneric(const CodecSpecificInfo* codec_specific_info,
|
|||||||
RTC_DCHECK_NOTREACHED() << "Unsupported codec.";
|
RTC_DCHECK_NOTREACHED() << "Unsupported codec.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
absl::optional<FrameDependencyStructure> RtpPayloadParams::GenericStructure(
|
||||||
|
const CodecSpecificInfo* codec_specific_info) {
|
||||||
|
if (codec_specific_info == nullptr) {
|
||||||
|
return absl::nullopt;
|
||||||
|
}
|
||||||
|
// This helper shouldn't be used when template structure is specified
|
||||||
|
// explicetly.
|
||||||
|
RTC_DCHECK(!codec_specific_info->template_structure.has_value());
|
||||||
|
switch (codec_specific_info->codecType) {
|
||||||
|
case VideoCodecType::kVideoCodecGeneric:
|
||||||
|
if (simulate_generic_structure_) {
|
||||||
|
return MinimalisticStructure(/*num_spatial_layers=*/1,
|
||||||
|
/*num_temporal_layer=*/1);
|
||||||
|
}
|
||||||
|
return absl::nullopt;
|
||||||
|
case VideoCodecType::kVideoCodecVP8:
|
||||||
|
return MinimalisticStructure(/*num_spatial_layers=*/1,
|
||||||
|
/*num_temporal_layer=*/kMaxTemporalStreams);
|
||||||
|
case VideoCodecType::kVideoCodecVP9: {
|
||||||
|
absl::optional<FrameDependencyStructure> structure =
|
||||||
|
MinimalisticStructure(
|
||||||
|
/*num_spatial_layers=*/kMaxSimulatedSpatialLayers,
|
||||||
|
/*num_temporal_layer=*/kMaxTemporalStreams);
|
||||||
|
const CodecSpecificInfoVP9& vp9 = codec_specific_info->codecSpecific.VP9;
|
||||||
|
if (vp9.ss_data_available && vp9.spatial_layer_resolution_present) {
|
||||||
|
RenderResolution first_valid;
|
||||||
|
RenderResolution last_valid;
|
||||||
|
for (size_t i = 0; i < vp9.num_spatial_layers; ++i) {
|
||||||
|
RenderResolution r(vp9.width[i], vp9.height[i]);
|
||||||
|
if (r.Valid()) {
|
||||||
|
if (!first_valid.Valid()) {
|
||||||
|
first_valid = r;
|
||||||
|
}
|
||||||
|
last_valid = r;
|
||||||
|
}
|
||||||
|
structure->resolutions.push_back(r);
|
||||||
|
}
|
||||||
|
if (!last_valid.Valid()) {
|
||||||
|
// No valid resolution found. Do not send resolutions.
|
||||||
|
structure->resolutions.clear();
|
||||||
|
} else {
|
||||||
|
structure->resolutions.resize(kMaxSimulatedSpatialLayers, last_valid);
|
||||||
|
// VP9 encoder wrapper may disable first few spatial layers by
|
||||||
|
// setting invalid resolution (0,0). `structure->resolutions`
|
||||||
|
// doesn't support invalid resolution, so reset them to something
|
||||||
|
// valid.
|
||||||
|
for (RenderResolution& r : structure->resolutions) {
|
||||||
|
if (!r.Valid()) {
|
||||||
|
r = first_valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return structure;
|
||||||
|
}
|
||||||
|
case VideoCodecType::kVideoCodecAV1:
|
||||||
|
case VideoCodecType::kVideoCodecH264:
|
||||||
|
case VideoCodecType::kVideoCodecMultiplex:
|
||||||
|
return absl::nullopt;
|
||||||
|
}
|
||||||
|
RTC_DCHECK_NOTREACHED() << "Unsupported codec.";
|
||||||
|
}
|
||||||
|
|
||||||
void RtpPayloadParams::GenericToGeneric(int64_t shared_frame_id,
|
void RtpPayloadParams::GenericToGeneric(int64_t shared_frame_id,
|
||||||
bool is_keyframe,
|
bool is_keyframe,
|
||||||
RTPVideoHeader* rtp_video_header) {
|
RTPVideoHeader* rtp_video_header) {
|
||||||
@ -426,49 +538,20 @@ void RtpPayloadParams::Vp8ToGeneric(const CodecSpecificInfoVP8& vp8_info,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameDependencyStructure RtpPayloadParams::MinimalisticStructure(
|
|
||||||
int num_spatial_layers,
|
|
||||||
int num_temporal_layers) {
|
|
||||||
RTC_DCHECK_LE(num_spatial_layers * num_temporal_layers, 32);
|
|
||||||
FrameDependencyStructure structure;
|
|
||||||
structure.num_decode_targets = num_spatial_layers * num_temporal_layers;
|
|
||||||
structure.num_chains = num_spatial_layers;
|
|
||||||
structure.templates.reserve(num_spatial_layers * num_temporal_layers);
|
|
||||||
for (int sid = 0; sid < num_spatial_layers; ++sid) {
|
|
||||||
for (int tid = 0; tid < num_temporal_layers; ++tid) {
|
|
||||||
FrameDependencyTemplate a_template;
|
|
||||||
a_template.spatial_id = sid;
|
|
||||||
a_template.temporal_id = tid;
|
|
||||||
for (int s = 0; s < num_spatial_layers; ++s) {
|
|
||||||
for (int t = 0; t < num_temporal_layers; ++t) {
|
|
||||||
// Prefer kSwitch indication for frames that is part of the decode
|
|
||||||
// target because dependency descriptor information generated in this
|
|
||||||
// class use kSwitch indications more often that kRequired, increasing
|
|
||||||
// the chance of a good (or complete) template match.
|
|
||||||
a_template.decode_target_indications.push_back(
|
|
||||||
sid <= s && tid <= t ? DecodeTargetIndication::kSwitch
|
|
||||||
: DecodeTargetIndication::kNotPresent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a_template.frame_diffs.push_back(tid == 0 ? num_spatial_layers *
|
|
||||||
num_temporal_layers
|
|
||||||
: num_spatial_layers);
|
|
||||||
a_template.chain_diffs.assign(structure.num_chains, 1);
|
|
||||||
structure.templates.push_back(a_template);
|
|
||||||
|
|
||||||
structure.decode_target_protected_by_chain.push_back(sid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return structure;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RtpPayloadParams::Vp9ToGeneric(const CodecSpecificInfoVP9& vp9_info,
|
void RtpPayloadParams::Vp9ToGeneric(const CodecSpecificInfoVP9& vp9_info,
|
||||||
int64_t shared_frame_id,
|
int64_t shared_frame_id,
|
||||||
RTPVideoHeader& rtp_video_header) {
|
RTPVideoHeader& rtp_video_header) {
|
||||||
const auto& vp9_header =
|
const auto& vp9_header =
|
||||||
absl::get<RTPVideoHeaderVP9>(rtp_video_header.video_type_header);
|
absl::get<RTPVideoHeaderVP9>(rtp_video_header.video_type_header);
|
||||||
const int num_spatial_layers = vp9_header.num_spatial_layers;
|
const int num_spatial_layers = kMaxSimulatedSpatialLayers;
|
||||||
|
const int num_active_spatial_layers = vp9_header.num_spatial_layers;
|
||||||
const int num_temporal_layers = kMaxTemporalStreams;
|
const int num_temporal_layers = kMaxTemporalStreams;
|
||||||
|
static_assert(num_spatial_layers <=
|
||||||
|
RtpGenericFrameDescriptor::kMaxSpatialLayers);
|
||||||
|
static_assert(num_temporal_layers <=
|
||||||
|
RtpGenericFrameDescriptor::kMaxTemporalLayers);
|
||||||
|
static_assert(num_spatial_layers <= DependencyDescriptor::kMaxSpatialIds);
|
||||||
|
static_assert(num_temporal_layers <= DependencyDescriptor::kMaxTemporalIds);
|
||||||
|
|
||||||
int spatial_index =
|
int spatial_index =
|
||||||
vp9_header.spatial_idx != kNoSpatialIdx ? vp9_header.spatial_idx : 0;
|
vp9_header.spatial_idx != kNoSpatialIdx ? vp9_header.spatial_idx : 0;
|
||||||
@ -477,7 +560,7 @@ void RtpPayloadParams::Vp9ToGeneric(const CodecSpecificInfoVP9& vp9_info,
|
|||||||
|
|
||||||
if (spatial_index >= num_spatial_layers ||
|
if (spatial_index >= num_spatial_layers ||
|
||||||
temporal_index >= num_temporal_layers ||
|
temporal_index >= num_temporal_layers ||
|
||||||
num_spatial_layers > RtpGenericFrameDescriptor::kMaxSpatialLayers) {
|
num_active_spatial_layers > num_spatial_layers) {
|
||||||
// Prefer to generate no generic layering than an inconsistent one.
|
// Prefer to generate no generic layering than an inconsistent one.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -541,6 +624,9 @@ void RtpPayloadParams::Vp9ToGeneric(const CodecSpecificInfoVP9& vp9_info,
|
|||||||
last_vp9_frame_id_[vp9_header.picture_id % kPictureDiffLimit][spatial_index] =
|
last_vp9_frame_id_[vp9_header.picture_id % kPictureDiffLimit][spatial_index] =
|
||||||
shared_frame_id;
|
shared_frame_id;
|
||||||
|
|
||||||
|
result.active_decode_targets =
|
||||||
|
((uint32_t{1} << num_temporal_layers * num_active_spatial_layers) - 1);
|
||||||
|
|
||||||
// Calculate chains, asuming chain includes all frames with temporal_id = 0
|
// Calculate chains, asuming chain includes all frames with temporal_id = 0
|
||||||
if (!vp9_header.inter_pic_predicted && !vp9_header.inter_layer_predicted) {
|
if (!vp9_header.inter_pic_predicted && !vp9_header.inter_layer_predicted) {
|
||||||
// Assume frames without dependencies also reset chains.
|
// Assume frames without dependencies also reset chains.
|
||||||
@ -548,8 +634,8 @@ void RtpPayloadParams::Vp9ToGeneric(const CodecSpecificInfoVP9& vp9_info,
|
|||||||
chain_last_frame_id_[sid] = -1;
|
chain_last_frame_id_[sid] = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.chain_diffs.resize(num_spatial_layers);
|
result.chain_diffs.resize(num_spatial_layers, 0);
|
||||||
for (int sid = 0; sid < num_spatial_layers; ++sid) {
|
for (int sid = 0; sid < num_active_spatial_layers; ++sid) {
|
||||||
if (chain_last_frame_id_[sid] == -1) {
|
if (chain_last_frame_id_[sid] == -1) {
|
||||||
result.chain_diffs[sid] = 0;
|
result.chain_diffs[sid] = 0;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -26,8 +26,6 @@
|
|||||||
|
|
||||||
namespace webrtc {
|
namespace webrtc {
|
||||||
|
|
||||||
class RtpRtcp;
|
|
||||||
|
|
||||||
// State for setting picture id and tl0 pic idx, for VP8 and VP9
|
// State for setting picture id and tl0 pic idx, for VP8 and VP9
|
||||||
// TODO(nisse): Make these properties not codec specific.
|
// TODO(nisse): Make these properties not codec specific.
|
||||||
class RtpPayloadParams final {
|
class RtpPayloadParams final {
|
||||||
@ -42,16 +40,10 @@ class RtpPayloadParams final {
|
|||||||
const CodecSpecificInfo* codec_specific_info,
|
const CodecSpecificInfo* codec_specific_info,
|
||||||
int64_t shared_frame_id);
|
int64_t shared_frame_id);
|
||||||
|
|
||||||
// Returns structure that aligns with simulated generic info. The templates
|
// Returns structure that aligns with simulated generic info generated by
|
||||||
// allow to produce valid dependency descriptor for any stream where
|
// `GetRtpVideoHeader` for the `codec_specific_info`
|
||||||
// `num_spatial_layers` * `num_temporal_layers` <= 32 (limited by
|
absl::optional<FrameDependencyStructure> GenericStructure(
|
||||||
// https://aomediacodec.github.io/av1-rtp-spec/#a82-syntax, see
|
const CodecSpecificInfo* codec_specific_info);
|
||||||
// template_fdiffs()). The set of the templates is not tuned for any paricular
|
|
||||||
// structure thus dependency descriptor would use more bytes on the wire than
|
|
||||||
// with tuned templates.
|
|
||||||
static FrameDependencyStructure MinimalisticStructure(
|
|
||||||
int num_spatial_layers,
|
|
||||||
int num_temporal_layers);
|
|
||||||
|
|
||||||
uint32_t ssrc() const;
|
uint32_t ssrc() const;
|
||||||
|
|
||||||
@ -136,6 +128,7 @@ class RtpPayloadParams final {
|
|||||||
RtpPayloadState state_;
|
RtpPayloadState state_;
|
||||||
|
|
||||||
const bool generic_picture_id_experiment_;
|
const bool generic_picture_id_experiment_;
|
||||||
|
const bool simulate_generic_structure_;
|
||||||
};
|
};
|
||||||
} // namespace webrtc
|
} // namespace webrtc
|
||||||
#endif // CALL_RTP_PAYLOAD_PARAMS_H_
|
#endif // CALL_RTP_PAYLOAD_PARAMS_H_
|
||||||
|
|||||||
@ -587,7 +587,8 @@ TEST(RtpPayloadParamsVp9ToGenericTest, NoScalability) {
|
|||||||
EXPECT_EQ(header.generic->decode_target_indications[0],
|
EXPECT_EQ(header.generic->decode_target_indications[0],
|
||||||
DecodeTargetIndication::kSwitch);
|
DecodeTargetIndication::kSwitch);
|
||||||
EXPECT_THAT(header.generic->dependencies, IsEmpty());
|
EXPECT_THAT(header.generic->dependencies, IsEmpty());
|
||||||
EXPECT_THAT(header.generic->chain_diffs, ElementsAre(0));
|
ASSERT_THAT(header.generic->chain_diffs, Not(IsEmpty()));
|
||||||
|
EXPECT_EQ(header.generic->chain_diffs[0], 0);
|
||||||
|
|
||||||
// Delta frame.
|
// Delta frame.
|
||||||
encoded_image._frameType = VideoFrameType::kVideoFrameDelta;
|
encoded_image._frameType = VideoFrameType::kVideoFrameDelta;
|
||||||
@ -605,8 +606,9 @@ TEST(RtpPayloadParamsVp9ToGenericTest, NoScalability) {
|
|||||||
EXPECT_EQ(header.generic->decode_target_indications[0],
|
EXPECT_EQ(header.generic->decode_target_indications[0],
|
||||||
DecodeTargetIndication::kSwitch);
|
DecodeTargetIndication::kSwitch);
|
||||||
EXPECT_THAT(header.generic->dependencies, ElementsAre(1));
|
EXPECT_THAT(header.generic->dependencies, ElementsAre(1));
|
||||||
|
ASSERT_THAT(header.generic->chain_diffs, Not(IsEmpty()));
|
||||||
// previous frame in the chain was frame#1,
|
// previous frame in the chain was frame#1,
|
||||||
EXPECT_THAT(header.generic->chain_diffs, ElementsAre(3 - 1));
|
EXPECT_EQ(header.generic->chain_diffs[0], 3 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
||||||
@ -670,7 +672,9 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
|||||||
|
|
||||||
ASSERT_TRUE(headers[0].generic);
|
ASSERT_TRUE(headers[0].generic);
|
||||||
int num_decode_targets = headers[0].generic->decode_target_indications.size();
|
int num_decode_targets = headers[0].generic->decode_target_indications.size();
|
||||||
|
int num_chains = headers[0].generic->chain_diffs.size();
|
||||||
ASSERT_GE(num_decode_targets, 2);
|
ASSERT_GE(num_decode_targets, 2);
|
||||||
|
ASSERT_GE(num_chains, 1);
|
||||||
|
|
||||||
for (int frame_idx = 0; frame_idx < 6; ++frame_idx) {
|
for (int frame_idx = 0; frame_idx < 6; ++frame_idx) {
|
||||||
const RTPVideoHeader& header = headers[frame_idx];
|
const RTPVideoHeader& header = headers[frame_idx];
|
||||||
@ -680,6 +684,7 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
|||||||
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
||||||
ASSERT_THAT(header.generic->decode_target_indications,
|
ASSERT_THAT(header.generic->decode_target_indications,
|
||||||
SizeIs(num_decode_targets));
|
SizeIs(num_decode_targets));
|
||||||
|
ASSERT_THAT(header.generic->chain_diffs, SizeIs(num_chains));
|
||||||
// Expect only T0 frames are needed for the 1st decode target.
|
// Expect only T0 frames are needed for the 1st decode target.
|
||||||
if (header.generic->temporal_index == 0) {
|
if (header.generic->temporal_index == 0) {
|
||||||
EXPECT_NE(header.generic->decode_target_indications[0],
|
EXPECT_NE(header.generic->decode_target_indications[0],
|
||||||
@ -694,10 +699,14 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Expect switch at every beginning of the pattern.
|
// Expect switch at every beginning of the pattern.
|
||||||
EXPECT_THAT(headers[0].generic->decode_target_indications,
|
EXPECT_THAT(headers[0].generic->decode_target_indications[0],
|
||||||
Each(DecodeTargetIndication::kSwitch));
|
DecodeTargetIndication::kSwitch);
|
||||||
EXPECT_THAT(headers[4].generic->decode_target_indications,
|
EXPECT_THAT(headers[0].generic->decode_target_indications[1],
|
||||||
Each(DecodeTargetIndication::kSwitch));
|
DecodeTargetIndication::kSwitch);
|
||||||
|
EXPECT_THAT(headers[4].generic->decode_target_indications[0],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
EXPECT_THAT(headers[4].generic->decode_target_indications[1],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
|
||||||
EXPECT_THAT(headers[0].generic->dependencies, IsEmpty()); // T0, 1
|
EXPECT_THAT(headers[0].generic->dependencies, IsEmpty()); // T0, 1
|
||||||
EXPECT_THAT(headers[1].generic->dependencies, ElementsAre(1)); // T1, 3
|
EXPECT_THAT(headers[1].generic->dependencies, ElementsAre(1)); // T1, 3
|
||||||
@ -706,12 +715,12 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith2Layers) {
|
|||||||
EXPECT_THAT(headers[4].generic->dependencies, ElementsAre(5)); // T0, 9
|
EXPECT_THAT(headers[4].generic->dependencies, ElementsAre(5)); // T0, 9
|
||||||
EXPECT_THAT(headers[5].generic->dependencies, ElementsAre(9)); // T1, 11
|
EXPECT_THAT(headers[5].generic->dependencies, ElementsAre(9)); // T1, 11
|
||||||
|
|
||||||
EXPECT_THAT(headers[0].generic->chain_diffs, ElementsAre(0));
|
EXPECT_THAT(headers[0].generic->chain_diffs[0], Eq(0));
|
||||||
EXPECT_THAT(headers[1].generic->chain_diffs, ElementsAre(2));
|
EXPECT_THAT(headers[1].generic->chain_diffs[0], Eq(2));
|
||||||
EXPECT_THAT(headers[2].generic->chain_diffs, ElementsAre(4));
|
EXPECT_THAT(headers[2].generic->chain_diffs[0], Eq(4));
|
||||||
EXPECT_THAT(headers[3].generic->chain_diffs, ElementsAre(2));
|
EXPECT_THAT(headers[3].generic->chain_diffs[0], Eq(2));
|
||||||
EXPECT_THAT(headers[4].generic->chain_diffs, ElementsAre(4));
|
EXPECT_THAT(headers[4].generic->chain_diffs[0], Eq(4));
|
||||||
EXPECT_THAT(headers[5].generic->chain_diffs, ElementsAre(2));
|
EXPECT_THAT(headers[5].generic->chain_diffs[0], Eq(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
||||||
@ -792,7 +801,9 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
|||||||
|
|
||||||
ASSERT_TRUE(headers[0].generic);
|
ASSERT_TRUE(headers[0].generic);
|
||||||
int num_decode_targets = headers[0].generic->decode_target_indications.size();
|
int num_decode_targets = headers[0].generic->decode_target_indications.size();
|
||||||
|
int num_chains = headers[0].generic->chain_diffs.size();
|
||||||
ASSERT_GE(num_decode_targets, 3);
|
ASSERT_GE(num_decode_targets, 3);
|
||||||
|
ASSERT_GE(num_chains, 1);
|
||||||
|
|
||||||
for (int frame_idx = 0; frame_idx < 9; ++frame_idx) {
|
for (int frame_idx = 0; frame_idx < 9; ++frame_idx) {
|
||||||
const RTPVideoHeader& header = headers[frame_idx];
|
const RTPVideoHeader& header = headers[frame_idx];
|
||||||
@ -801,6 +812,7 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
|||||||
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
||||||
ASSERT_THAT(header.generic->decode_target_indications,
|
ASSERT_THAT(header.generic->decode_target_indications,
|
||||||
SizeIs(num_decode_targets));
|
SizeIs(num_decode_targets));
|
||||||
|
ASSERT_THAT(header.generic->chain_diffs, SizeIs(num_chains));
|
||||||
// Expect only T0 frames are needed for the 1st decode target.
|
// Expect only T0 frames are needed for the 1st decode target.
|
||||||
if (header.generic->temporal_index == 0) {
|
if (header.generic->temporal_index == 0) {
|
||||||
EXPECT_NE(header.generic->decode_target_indications[0],
|
EXPECT_NE(header.generic->decode_target_indications[0],
|
||||||
@ -835,8 +847,12 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
|||||||
// Expect switch at every beginning of the pattern.
|
// Expect switch at every beginning of the pattern.
|
||||||
EXPECT_THAT(headers[0].generic->decode_target_indications,
|
EXPECT_THAT(headers[0].generic->decode_target_indications,
|
||||||
Each(DecodeTargetIndication::kSwitch));
|
Each(DecodeTargetIndication::kSwitch));
|
||||||
EXPECT_THAT(headers[8].generic->decode_target_indications,
|
EXPECT_THAT(headers[8].generic->decode_target_indications[0],
|
||||||
Each(DecodeTargetIndication::kSwitch));
|
DecodeTargetIndication::kSwitch);
|
||||||
|
EXPECT_THAT(headers[8].generic->decode_target_indications[1],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
EXPECT_THAT(headers[8].generic->decode_target_indications[2],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
|
||||||
EXPECT_THAT(headers[0].generic->dependencies, IsEmpty()); // T0, 1
|
EXPECT_THAT(headers[0].generic->dependencies, IsEmpty()); // T0, 1
|
||||||
EXPECT_THAT(headers[1].generic->dependencies, ElementsAre(1)); // T2, 3
|
EXPECT_THAT(headers[1].generic->dependencies, ElementsAre(1)); // T2, 3
|
||||||
@ -848,15 +864,15 @@ TEST(RtpPayloadParamsVp9ToGenericTest, TemporalScalabilityWith3Layers) {
|
|||||||
EXPECT_THAT(headers[7].generic->dependencies, ElementsAre(13)); // T2, 15
|
EXPECT_THAT(headers[7].generic->dependencies, ElementsAre(13)); // T2, 15
|
||||||
EXPECT_THAT(headers[8].generic->dependencies, ElementsAre(9)); // T0, 17
|
EXPECT_THAT(headers[8].generic->dependencies, ElementsAre(9)); // T0, 17
|
||||||
|
|
||||||
EXPECT_THAT(headers[0].generic->chain_diffs, ElementsAre(0));
|
EXPECT_THAT(headers[0].generic->chain_diffs[0], Eq(0));
|
||||||
EXPECT_THAT(headers[1].generic->chain_diffs, ElementsAre(2));
|
EXPECT_THAT(headers[1].generic->chain_diffs[0], Eq(2));
|
||||||
EXPECT_THAT(headers[2].generic->chain_diffs, ElementsAre(4));
|
EXPECT_THAT(headers[2].generic->chain_diffs[0], Eq(4));
|
||||||
EXPECT_THAT(headers[3].generic->chain_diffs, ElementsAre(6));
|
EXPECT_THAT(headers[3].generic->chain_diffs[0], Eq(6));
|
||||||
EXPECT_THAT(headers[4].generic->chain_diffs, ElementsAre(8));
|
EXPECT_THAT(headers[4].generic->chain_diffs[0], Eq(8));
|
||||||
EXPECT_THAT(headers[5].generic->chain_diffs, ElementsAre(2));
|
EXPECT_THAT(headers[5].generic->chain_diffs[0], Eq(2));
|
||||||
EXPECT_THAT(headers[6].generic->chain_diffs, ElementsAre(4));
|
EXPECT_THAT(headers[6].generic->chain_diffs[0], Eq(4));
|
||||||
EXPECT_THAT(headers[7].generic->chain_diffs, ElementsAre(6));
|
EXPECT_THAT(headers[7].generic->chain_diffs[0], Eq(6));
|
||||||
EXPECT_THAT(headers[8].generic->chain_diffs, ElementsAre(8));
|
EXPECT_THAT(headers[8].generic->chain_diffs[0], Eq(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RtpPayloadParamsVp9ToGenericTest, SpatialScalabilityKSvc) {
|
TEST(RtpPayloadParamsVp9ToGenericTest, SpatialScalabilityKSvc) {
|
||||||
@ -916,7 +932,9 @@ TEST(RtpPayloadParamsVp9ToGenericTest, SpatialScalabilityKSvc) {
|
|||||||
// Rely on implementation detail there are always kMaxTemporalStreams temporal
|
// Rely on implementation detail there are always kMaxTemporalStreams temporal
|
||||||
// layers assumed, in particular assume Decode Target#0 matches layer S0T0,
|
// layers assumed, in particular assume Decode Target#0 matches layer S0T0,
|
||||||
// and Decode Target#kMaxTemporalStreams matches layer S1T0.
|
// and Decode Target#kMaxTemporalStreams matches layer S1T0.
|
||||||
ASSERT_EQ(num_decode_targets, kMaxTemporalStreams * 2);
|
ASSERT_GE(num_decode_targets, kMaxTemporalStreams * 2);
|
||||||
|
int num_chains = headers[0].generic->chain_diffs.size();
|
||||||
|
ASSERT_GE(num_chains, 2);
|
||||||
|
|
||||||
for (int frame_idx = 0; frame_idx < 4; ++frame_idx) {
|
for (int frame_idx = 0; frame_idx < 4; ++frame_idx) {
|
||||||
const RTPVideoHeader& header = headers[frame_idx];
|
const RTPVideoHeader& header = headers[frame_idx];
|
||||||
@ -926,6 +944,7 @@ TEST(RtpPayloadParamsVp9ToGenericTest, SpatialScalabilityKSvc) {
|
|||||||
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
||||||
ASSERT_THAT(header.generic->decode_target_indications,
|
ASSERT_THAT(header.generic->decode_target_indications,
|
||||||
SizeIs(num_decode_targets));
|
SizeIs(num_decode_targets));
|
||||||
|
ASSERT_THAT(header.generic->chain_diffs, SizeIs(num_chains));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expect S0 key frame is switch for both Decode Targets.
|
// Expect S0 key frame is switch for both Decode Targets.
|
||||||
@ -953,10 +972,114 @@ TEST(RtpPayloadParamsVp9ToGenericTest, SpatialScalabilityKSvc) {
|
|||||||
EXPECT_THAT(headers[2].generic->dependencies, ElementsAre(1)); // S0, 5
|
EXPECT_THAT(headers[2].generic->dependencies, ElementsAre(1)); // S0, 5
|
||||||
EXPECT_THAT(headers[3].generic->dependencies, ElementsAre(3)); // S1, 7
|
EXPECT_THAT(headers[3].generic->dependencies, ElementsAre(3)); // S1, 7
|
||||||
|
|
||||||
EXPECT_THAT(headers[0].generic->chain_diffs, ElementsAre(0, 0));
|
EXPECT_THAT(headers[0].generic->chain_diffs[0], Eq(0));
|
||||||
EXPECT_THAT(headers[1].generic->chain_diffs, ElementsAre(2, 2));
|
EXPECT_THAT(headers[0].generic->chain_diffs[1], Eq(0));
|
||||||
EXPECT_THAT(headers[2].generic->chain_diffs, ElementsAre(4, 2));
|
EXPECT_THAT(headers[1].generic->chain_diffs[0], Eq(2));
|
||||||
EXPECT_THAT(headers[3].generic->chain_diffs, ElementsAre(2, 4));
|
EXPECT_THAT(headers[1].generic->chain_diffs[1], Eq(2));
|
||||||
|
EXPECT_THAT(headers[2].generic->chain_diffs[0], Eq(4));
|
||||||
|
EXPECT_THAT(headers[2].generic->chain_diffs[1], Eq(2));
|
||||||
|
EXPECT_THAT(headers[3].generic->chain_diffs[0], Eq(2));
|
||||||
|
EXPECT_THAT(headers[3].generic->chain_diffs[1], Eq(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RtpPayloadParamsVp9ToGenericTest,
|
||||||
|
IncreaseNumberOfSpatialLayersOnDeltaFrame) {
|
||||||
|
// S1 5--
|
||||||
|
// | ...
|
||||||
|
// S0 1---3--
|
||||||
|
RtpPayloadState state;
|
||||||
|
RtpPayloadParams params(/*ssrc=*/123, &state, FieldTrialBasedConfig());
|
||||||
|
|
||||||
|
EncodedImage image;
|
||||||
|
CodecSpecificInfo info;
|
||||||
|
info.codecType = kVideoCodecVP9;
|
||||||
|
info.codecSpecific.VP9.num_spatial_layers = 1;
|
||||||
|
info.codecSpecific.VP9.first_frame_in_picture = true;
|
||||||
|
|
||||||
|
RTPVideoHeader headers[3];
|
||||||
|
// Key frame.
|
||||||
|
image._frameType = VideoFrameType::kVideoFrameKey;
|
||||||
|
image.SetSpatialIndex(0);
|
||||||
|
info.codecSpecific.VP9.inter_pic_predicted = false;
|
||||||
|
info.codecSpecific.VP9.inter_layer_predicted = false;
|
||||||
|
info.codecSpecific.VP9.non_ref_for_inter_layer_pred = true;
|
||||||
|
info.codecSpecific.VP9.num_ref_pics = 0;
|
||||||
|
info.codecSpecific.VP9.first_frame_in_picture = true;
|
||||||
|
info.end_of_picture = true;
|
||||||
|
headers[0] = params.GetRtpVideoHeader(image, &info, /*shared_frame_id=*/1);
|
||||||
|
|
||||||
|
// S0 delta frame.
|
||||||
|
image._frameType = VideoFrameType::kVideoFrameDelta;
|
||||||
|
info.codecSpecific.VP9.num_spatial_layers = 2;
|
||||||
|
info.codecSpecific.VP9.non_ref_for_inter_layer_pred = false;
|
||||||
|
info.codecSpecific.VP9.first_frame_in_picture = true;
|
||||||
|
info.codecSpecific.VP9.inter_pic_predicted = true;
|
||||||
|
info.codecSpecific.VP9.num_ref_pics = 1;
|
||||||
|
info.codecSpecific.VP9.p_diff[0] = 1;
|
||||||
|
info.end_of_picture = false;
|
||||||
|
headers[1] = params.GetRtpVideoHeader(image, &info, /*shared_frame_id=*/3);
|
||||||
|
|
||||||
|
// S1 delta frame.
|
||||||
|
image.SetSpatialIndex(1);
|
||||||
|
info.codecSpecific.VP9.inter_layer_predicted = true;
|
||||||
|
info.codecSpecific.VP9.non_ref_for_inter_layer_pred = true;
|
||||||
|
info.codecSpecific.VP9.first_frame_in_picture = false;
|
||||||
|
info.codecSpecific.VP9.inter_pic_predicted = false;
|
||||||
|
info.end_of_picture = true;
|
||||||
|
headers[2] = params.GetRtpVideoHeader(image, &info, /*shared_frame_id=*/5);
|
||||||
|
|
||||||
|
ASSERT_TRUE(headers[0].generic);
|
||||||
|
int num_decode_targets = headers[0].generic->decode_target_indications.size();
|
||||||
|
int num_chains = headers[0].generic->chain_diffs.size();
|
||||||
|
// Rely on implementation detail there are always kMaxTemporalStreams temporal
|
||||||
|
// layers. In particular assume Decode Target#0 matches layer S0T0, and
|
||||||
|
// Decode Target#kMaxTemporalStreams matches layer S1T0.
|
||||||
|
static constexpr int kS0T0 = 0;
|
||||||
|
static constexpr int kS1T0 = kMaxTemporalStreams;
|
||||||
|
ASSERT_GE(num_decode_targets, 2);
|
||||||
|
ASSERT_GE(num_chains, 2);
|
||||||
|
|
||||||
|
for (int frame_idx = 0; frame_idx < 3; ++frame_idx) {
|
||||||
|
const RTPVideoHeader& header = headers[frame_idx];
|
||||||
|
ASSERT_TRUE(header.generic);
|
||||||
|
EXPECT_EQ(header.generic->temporal_index, 0);
|
||||||
|
EXPECT_EQ(header.generic->frame_id, 1 + 2 * frame_idx);
|
||||||
|
ASSERT_THAT(header.generic->decode_target_indications,
|
||||||
|
SizeIs(num_decode_targets));
|
||||||
|
ASSERT_THAT(header.generic->chain_diffs, SizeIs(num_chains));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_TRUE(headers[0].generic->active_decode_targets[kS0T0]);
|
||||||
|
EXPECT_FALSE(headers[0].generic->active_decode_targets[kS1T0]);
|
||||||
|
|
||||||
|
EXPECT_TRUE(headers[1].generic->active_decode_targets[kS0T0]);
|
||||||
|
EXPECT_TRUE(headers[1].generic->active_decode_targets[kS1T0]);
|
||||||
|
|
||||||
|
EXPECT_TRUE(headers[2].generic->active_decode_targets[kS0T0]);
|
||||||
|
EXPECT_TRUE(headers[2].generic->active_decode_targets[kS1T0]);
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[0].generic->decode_target_indications[kS0T0],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[1].generic->decode_target_indications[kS0T0],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[2].generic->decode_target_indications[kS0T0],
|
||||||
|
DecodeTargetIndication::kNotPresent);
|
||||||
|
EXPECT_EQ(headers[2].generic->decode_target_indications[kS1T0],
|
||||||
|
DecodeTargetIndication::kSwitch);
|
||||||
|
|
||||||
|
EXPECT_THAT(headers[0].generic->dependencies, IsEmpty()); // S0, 1
|
||||||
|
EXPECT_THAT(headers[1].generic->dependencies, ElementsAre(1)); // S0, 3
|
||||||
|
EXPECT_THAT(headers[2].generic->dependencies, ElementsAre(3)); // S1, 5
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[0].generic->chain_diffs[0], 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[1].generic->chain_diffs[0], 2);
|
||||||
|
EXPECT_EQ(headers[1].generic->chain_diffs[1], 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(headers[2].generic->chain_diffs[0], 2);
|
||||||
|
EXPECT_EQ(headers[2].generic->chain_diffs[1], 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RtpPayloadParamsH264ToGenericTest : public ::testing::Test {
|
class RtpPayloadParamsH264ToGenericTest : public ::testing::Test {
|
||||||
|
|||||||
@ -378,9 +378,6 @@ RtpVideoSender::RtpVideoSender(
|
|||||||
field_trials_.Lookup("WebRTC-Video-UseFrameRateForOverhead"),
|
field_trials_.Lookup("WebRTC-Video-UseFrameRateForOverhead"),
|
||||||
"Enabled")),
|
"Enabled")),
|
||||||
has_packet_feedback_(TransportSeqNumExtensionConfigured(rtp_config)),
|
has_packet_feedback_(TransportSeqNumExtensionConfigured(rtp_config)),
|
||||||
simulate_generic_structure_(absl::StartsWith(
|
|
||||||
field_trials_.Lookup("WebRTC-GenericCodecDependencyDescriptor"),
|
|
||||||
"Enabled")),
|
|
||||||
active_(false),
|
active_(false),
|
||||||
fec_controller_(std::move(fec_controller)),
|
fec_controller_(std::move(fec_controller)),
|
||||||
fec_allowed_(true),
|
fec_allowed_(true),
|
||||||
@ -603,32 +600,10 @@ EncodedImageCallback::Result RtpVideoSender::OnEncodedImage(
|
|||||||
RTPSenderVideo& sender_video = *rtp_streams_[stream_index].sender_video;
|
RTPSenderVideo& sender_video = *rtp_streams_[stream_index].sender_video;
|
||||||
if (codec_specific_info && codec_specific_info->template_structure) {
|
if (codec_specific_info && codec_specific_info->template_structure) {
|
||||||
sender_video.SetVideoStructure(&*codec_specific_info->template_structure);
|
sender_video.SetVideoStructure(&*codec_specific_info->template_structure);
|
||||||
} else if (codec_specific_info &&
|
} else if (absl::optional<FrameDependencyStructure> structure =
|
||||||
codec_specific_info->codecType == kVideoCodecVP8) {
|
params_[stream_index].GenericStructure(
|
||||||
FrameDependencyStructure structure =
|
codec_specific_info)) {
|
||||||
RtpPayloadParams::MinimalisticStructure(/*num_spatial_layers=*/1,
|
sender_video.SetVideoStructure(&*structure);
|
||||||
kMaxTemporalStreams);
|
|
||||||
sender_video.SetVideoStructure(&structure);
|
|
||||||
} else if (codec_specific_info &&
|
|
||||||
codec_specific_info->codecType == kVideoCodecVP9) {
|
|
||||||
const CodecSpecificInfoVP9& vp9 = codec_specific_info->codecSpecific.VP9;
|
|
||||||
|
|
||||||
FrameDependencyStructure structure =
|
|
||||||
RtpPayloadParams::MinimalisticStructure(vp9.num_spatial_layers,
|
|
||||||
kMaxTemporalStreams);
|
|
||||||
if (vp9.ss_data_available && vp9.spatial_layer_resolution_present) {
|
|
||||||
for (size_t i = 0; i < vp9.num_spatial_layers; ++i) {
|
|
||||||
structure.resolutions.emplace_back(vp9.width[i], vp9.height[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sender_video.SetVideoStructure(&structure);
|
|
||||||
} else if (simulate_generic_structure_ && codec_specific_info &&
|
|
||||||
codec_specific_info->codecType == kVideoCodecGeneric) {
|
|
||||||
FrameDependencyStructure structure =
|
|
||||||
RtpPayloadParams::MinimalisticStructure(
|
|
||||||
/*num_spatial_layers=*/1,
|
|
||||||
/*num_temporal_layers=*/1);
|
|
||||||
sender_video.SetVideoStructure(&structure);
|
|
||||||
} else {
|
} else {
|
||||||
sender_video.SetVideoStructure(nullptr);
|
sender_video.SetVideoStructure(nullptr);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,7 +170,6 @@ class RtpVideoSender : public RtpVideoSenderInterface,
|
|||||||
const bool send_side_bwe_with_overhead_;
|
const bool send_side_bwe_with_overhead_;
|
||||||
const bool use_frame_rate_for_overhead_;
|
const bool use_frame_rate_for_overhead_;
|
||||||
const bool has_packet_feedback_;
|
const bool has_packet_feedback_;
|
||||||
const bool simulate_generic_structure_;
|
|
||||||
|
|
||||||
// Semantically equivalent to checking for `transport_->GetWorkerQueue()`
|
// Semantically equivalent to checking for `transport_->GetWorkerQueue()`
|
||||||
// but some tests need to be updated to call from the correct context.
|
// but some tests need to be updated to call from the correct context.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user