Commit 190aa84d by bachinger Committed by Ian Baker

Do not manipulate the duration of SSAI periods in FakeTimeline

#minor-release

PiperOrigin-RevId: 428727560
parent 76c87462
...@@ -15,8 +15,12 @@ ...@@ -15,8 +15,12 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.AUDIO_FORMAT;
import static com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.VIDEO_FORMAT;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
...@@ -33,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; ...@@ -33,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource;
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
...@@ -44,6 +49,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; ...@@ -44,6 +49,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -818,10 +825,11 @@ public final class MediaPeriodQueueTest { ...@@ -818,10 +825,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodTimeline_rollForward() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodTimeline_rollForward()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 0, /* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ true, /* isAdPeriodFlags...= */ true,
...@@ -852,10 +860,11 @@ public final class MediaPeriodQueueTest { ...@@ -852,10 +860,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodAllAdsPlayed_seekNotAdjusted() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodAllAdsPlayed_seekNotAdjusted()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 4, /* numberOfPlayedAds= */ 4,
/* isAdPeriodFlags...= */ true, /* isAdPeriodFlags...= */ true,
...@@ -886,10 +895,11 @@ public final class MediaPeriodQueueTest { ...@@ -886,10 +895,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodFirstTwoAdsPlayed_rollForward() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_behindAdInMultiPeriodFirstTwoAdsPlayed_rollForward()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 2, /* numberOfPlayedAds= */ 2,
/* isAdPeriodFlags...= */ true, /* isAdPeriodFlags...= */ true,
...@@ -911,10 +921,11 @@ public final class MediaPeriodQueueTest { ...@@ -911,10 +921,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_beforeAdInMultiPeriodTimeline_seekNotAdjusted() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_beforeAdInMultiPeriodTimeline_seekNotAdjusted()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true); windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true);
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId =
...@@ -929,10 +940,11 @@ public final class MediaPeriodQueueTest { ...@@ -929,10 +940,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toUnplayedAdInMultiPeriodTimeline_resolvedAsAd() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toUnplayedAdInMultiPeriodTimeline_resolvedAsAd()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true, false); windowId, /* numberOfPlayedAds= */ 0, /* isAdPeriodFlags...= */ false, true, false);
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId =
...@@ -947,10 +959,11 @@ public final class MediaPeriodQueueTest { ...@@ -947,10 +959,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedAdInMultiPeriodTimeline_skipPlayedAd() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedAdInMultiPeriodTimeline_skipPlayedAd()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, /* numberOfPlayedAds= */ 1, /* isAdPeriodFlags...= */ false, true, false); windowId, /* numberOfPlayedAds= */ 1, /* isAdPeriodFlags...= */ false, true, false);
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId =
...@@ -965,12 +978,12 @@ public final class MediaPeriodQueueTest { ...@@ -965,12 +978,12 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toStartOfWindowPlayedAdPreroll_skipsPlayedPrerolls() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toStartOfWindowPlayedAdPreroll_skipsPlayedPrerolls()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, /* numberOfPlayedAds= */ 2, /* isAdPeriodFlags...= */ true, true, false); windowId, /* numberOfPlayedAds= */ 2, /* isAdPeriodFlags...= */ true, true, false);
MediaPeriodId mediaPeriodId = MediaPeriodId mediaPeriodId =
mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange( mediaPeriodQueue.resolveMediaPeriodIdForAdsAfterPeriodPositionChange(
timeline, new Pair<>(windowId, 0), /* positionUs= */ 0); timeline, new Pair<>(windowId, 0), /* positionUs= */ 0);
...@@ -983,10 +996,11 @@ public final class MediaPeriodQueueTest { ...@@ -983,10 +996,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedPostrolls_skipsAllButLastPostroll() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_toPlayedPostrolls_skipsAllButLastPostroll()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 4, /* numberOfPlayedAds= */ 4,
/* isAdPeriodFlags...= */ false, /* isAdPeriodFlags...= */ false,
...@@ -1007,10 +1021,11 @@ public final class MediaPeriodQueueTest { ...@@ -1007,10 +1021,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_consecutiveContentPeriods_rollForward() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_consecutiveContentPeriods_rollForward()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 0, /* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ true, /* isAdPeriodFlags...= */ true,
...@@ -1030,10 +1045,11 @@ public final class MediaPeriodQueueTest { ...@@ -1030,10 +1045,11 @@ public final class MediaPeriodQueueTest {
@Test @Test
public void public void
resolveMediaPeriodIdForAdsAfterPeriodPositionChange_onlyConsecutiveContentPeriods_seekNotAdjusted() { resolveMediaPeriodIdForAdsAfterPeriodPositionChange_onlyConsecutiveContentPeriods_seekNotAdjusted()
throws InterruptedException {
Object windowId = new Object(); Object windowId = new Object();
FakeTimeline timeline = Timeline timeline =
FakeTimeline.createMultiPeriodAdTimeline( createMultiPeriodServerSideInsertedTimeline(
windowId, windowId,
/* numberOfPlayedAds= */ 0, /* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false, /* isAdPeriodFlags...= */ false,
...@@ -1245,4 +1261,29 @@ public final class MediaPeriodQueueTest { ...@@ -1245,4 +1261,29 @@ public final class MediaPeriodQueueTest {
} }
return length; return length;
} }
private static Timeline createMultiPeriodServerSideInsertedTimeline(
Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags)
throws InterruptedException {
FakeTimeline timeline =
FakeTimeline.createMultiPeriodAdTimeline(windowId, numberOfPlayedAds, isAdPeriodFlags);
ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource =
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(timeline, VIDEO_FORMAT, AUDIO_FORMAT), contentTimeline -> false);
serverSideAdInsertionMediaSource.setAdPlaybackStates(
timeline.getAdPlaybackStates(/* windowIndex= */ 0));
AtomicReference<Timeline> serverSideAdInsertionTimelineRef = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1);
serverSideAdInsertionMediaSource.prepareSource(
(source, serverSideInsertedAdTimeline) -> {
serverSideAdInsertionTimelineRef.set(serverSideInsertedAdTimeline);
countDownLatch.countDown();
},
/* mediaTransferListener= */ null,
new PlayerId());
if (!countDownLatch.await(/* timeout= */ 2, SECONDS)) {
fail();
}
return serverSideAdInsertionTimelineRef.get();
}
} }
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; import static com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static com.google.android.exoplayer2.util.Util.sum;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
...@@ -321,14 +320,15 @@ public final class FakeTimeline extends Timeline { ...@@ -321,14 +320,15 @@ public final class FakeTimeline extends Timeline {
public static FakeTimeline createMultiPeriodAdTimeline( public static FakeTimeline createMultiPeriodAdTimeline(
Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) { Object windowId, int numberOfPlayedAds, boolean... isAdPeriodFlags) {
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / isAdPeriodFlags.length; long periodDurationUs = DEFAULT_WINDOW_DURATION_US / isAdPeriodFlags.length;
AdPlaybackState contentPeriodState = new AdPlaybackState(/* adsId= */ "adsId");
AdPlaybackState firstAdPeriodState = AdPlaybackState firstAdPeriodState =
new AdPlaybackState(/* adsId= */ "adsId", /* adGroupTimesUs... */ 0) contentPeriodState
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimesUs */ 0)
.withAdCount(/* adGroupIndex= */ 0, 1) .withAdCount(/* adGroupIndex= */ 0, 1)
.withAdDurationsUs( .withAdDurationsUs(
/* adGroupIndex= */ 0, DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs) /* adGroupIndex= */ 0, DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true); .withIsServerSideInserted(/* adGroupIndex= */ 0, true);
AdPlaybackState commonAdPeriodState = firstAdPeriodState.withAdDurationsUs(0, periodDurationUs); AdPlaybackState commonAdPeriodState = firstAdPeriodState.withAdDurationsUs(0, periodDurationUs);
AdPlaybackState contentPeriodState = new AdPlaybackState(/* adsId= */ "adsId");
List<AdPlaybackState> adPlaybackStates = new ArrayList<>(); List<AdPlaybackState> adPlaybackStates = new ArrayList<>();
int playedAdsCounter = 0; int playedAdsCounter = 0;
...@@ -522,9 +522,7 @@ public final class FakeTimeline extends Timeline { ...@@ -522,9 +522,7 @@ public final class FakeTimeline extends Timeline {
id, id,
uid, uid,
windowIndex, windowIndex,
periodDurationUs == C.TIME_UNSET periodDurationUs,
? C.TIME_UNSET
: periodDurationUs - getServerSideAdInsertionAdDurationUs(adPlaybackState),
positionInWindowUs, positionInWindowUs,
adPlaybackState, adPlaybackState,
windowDefinition.isPlaceholder); windowDefinition.isPlaceholder);
...@@ -575,15 +573,4 @@ public final class FakeTimeline extends Timeline { ...@@ -575,15 +573,4 @@ public final class FakeTimeline extends Timeline {
} }
return windowDefinitions; return windowDefinitions;
} }
private static long getServerSideAdInsertionAdDurationUs(AdPlaybackState adPlaybackState) {
long adDurationUs = 0;
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(i);
if (adGroup.isServerSideInserted) {
adDurationUs += sum(adGroup.durationsUs);
}
}
return adDurationUs;
}
} }
...@@ -45,20 +45,21 @@ public class FakeTimelineTest { ...@@ -45,20 +45,21 @@ public class FakeTimelineTest {
true, true,
true, true,
false, false,
true,
true); true);
assertThat(timeline.getWindowCount()).isEqualTo(1); assertThat(timeline.getWindowCount()).isEqualTo(1);
assertThat(timeline.getPeriodCount()).isEqualTo(7); assertThat(timeline.getPeriodCount()).isEqualTo(8);
// Assert content periods and window duration. // Assert content periods and window duration.
Timeline.Period contentPeriod1 = timeline.getPeriod(/* periodIndex= */ 1, period); Timeline.Period contentPeriod1 = timeline.getPeriod(/* periodIndex= */ 1, period);
Timeline.Period contentPeriod5 = timeline.getPeriod(/* periodIndex= */ 5, period); Timeline.Period contentPeriod5 = timeline.getPeriod(/* periodIndex= */ 5, period);
assertThat(contentPeriod1.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 7); assertThat(contentPeriod1.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 8);
assertThat(contentPeriod5.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 7); assertThat(contentPeriod5.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 8);
assertThat(contentPeriod1.getAdGroupCount()).isEqualTo(0); assertThat(contentPeriod1.getAdGroupCount()).isEqualTo(0);
assertThat(contentPeriod5.getAdGroupCount()).isEqualTo(0); assertThat(contentPeriod5.getAdGroupCount()).isEqualTo(0);
timeline.getWindow(/* windowIndex= */ 0, window); timeline.getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(windowId); assertThat(window.uid).isEqualTo(windowId);
assertThat(window.durationUs).isEqualTo(contentPeriod1.durationUs + contentPeriod5.durationUs); assertThat(window.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US);
assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US); assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US);
// Assert ad periods. // Assert ad periods.
int[] adIndices = {0, 2, 3, 4, 6}; int[] adIndices = {0, 2, 3, 4, 6};
...@@ -67,7 +68,6 @@ public class FakeTimelineTest { ...@@ -67,7 +68,6 @@ public class FakeTimelineTest {
Timeline.Period adPeriod = timeline.getPeriod(periodIndex, period); Timeline.Period adPeriod = timeline.getPeriod(periodIndex, period);
assertThat(adPeriod.isServerSideInsertedAdGroup(0)).isTrue(); assertThat(adPeriod.isServerSideInsertedAdGroup(0)).isTrue();
assertThat(adPeriod.getAdGroupCount()).isEqualTo(1); assertThat(adPeriod.getAdGroupCount()).isEqualTo(1);
assertThat(adPeriod.durationUs).isEqualTo(0);
if (adPeriod.getAdGroupCount() > 0) { if (adPeriod.getAdGroupCount() > 0) {
if (adCounter < numberOfPlayedAds) { if (adCounter < numberOfPlayedAds) {
assertThat(adPeriod.getAdState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)) assertThat(adPeriod.getAdState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
...@@ -79,8 +79,9 @@ public class FakeTimelineTest { ...@@ -79,8 +79,9 @@ public class FakeTimelineTest {
adCounter++; adCounter++;
} }
long expectedDurationUs = long expectedDurationUs =
(DEFAULT_WINDOW_DURATION_US / 7) (DEFAULT_WINDOW_DURATION_US / 8)
+ (periodIndex == 0 ? DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US : 0); + (periodIndex == 0 ? DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US : 0);
assertThat(adPeriod.durationUs).isEqualTo(expectedDurationUs);
assertThat(adPeriod.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)) assertThat(adPeriod.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
.isEqualTo(expectedDurationUs); .isEqualTo(expectedDurationUs);
} }
...@@ -100,7 +101,7 @@ public class FakeTimelineTest { ...@@ -100,7 +101,7 @@ public class FakeTimelineTest {
timeline.getWindow(/* windowIndex= */ 0, window); timeline.getWindow(/* windowIndex= */ 0, window);
// Assert content periods and window duration. // Assert content periods and window duration.
assertThat(window.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US / 2); assertThat(window.durationUs).isEqualTo(DEFAULT_WINDOW_DURATION_US);
assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US); assertThat(window.positionInFirstPeriodUs).isEqualTo(DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment