Mark played ads in multi-period VOD streams
#minor-release PiperOrigin-RevId: 425842813
This commit is contained in:
parent
c935bce07d
commit
3d9a56ad1f
@ -17,10 +17,12 @@ package androidx.media3.exoplayer.ima;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
import static androidx.media3.common.util.Util.msToUs;
|
||||||
import static androidx.media3.common.util.Util.secToUs;
|
import static androidx.media3.common.util.Util.secToUs;
|
||||||
import static androidx.media3.common.util.Util.sum;
|
import static androidx.media3.common.util.Util.sum;
|
||||||
import static androidx.media3.common.util.Util.usToMs;
|
import static androidx.media3.common.util.Util.usToMs;
|
||||||
import static androidx.media3.exoplayer.ima.ImaUtil.expandAdGroupPlaceholder;
|
import static androidx.media3.exoplayer.ima.ImaUtil.expandAdGroupPlaceholder;
|
||||||
|
import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
|
||||||
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdPlaybackStateForPeriods;
|
import static androidx.media3.exoplayer.ima.ImaUtil.splitAdPlaybackStateForPeriods;
|
||||||
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationAndPropagate;
|
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationAndPropagate;
|
||||||
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup;
|
import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup;
|
||||||
@ -30,6 +32,7 @@ import static java.lang.Math.min;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -620,18 +623,26 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldPosition.adGroupIndex != C.INDEX_UNSET && newPosition.adGroupIndex == C.INDEX_UNSET) {
|
if (oldPosition.adGroupIndex != C.INDEX_UNSET) {
|
||||||
AdPlaybackState newAdPlaybackState = adPlaybackState;
|
int adGroupIndex = oldPosition.adGroupIndex;
|
||||||
for (int i = 0; i <= oldPosition.adIndexInAdGroup; i++) {
|
int adIndexInAdGroup = oldPosition.adIndexInAdGroup;
|
||||||
int state = newAdPlaybackState.getAdGroup(oldPosition.adGroupIndex).states[i];
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
if (state != AdPlaybackState.AD_STATE_SKIPPED
|
Timeline.Window window =
|
||||||
&& state != AdPlaybackState.AD_STATE_ERROR) {
|
timeline.getWindow(oldPosition.mediaItemIndex, new Timeline.Window());
|
||||||
newAdPlaybackState =
|
if (window.lastPeriodIndex > window.firstPeriodIndex) {
|
||||||
newAdPlaybackState.withPlayedAd(
|
// Map adGroupIndex and adIndexInAdGroup to multi-period window.
|
||||||
oldPosition.adGroupIndex, /* adIndexInAdGroup= */ i);
|
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
|
||||||
}
|
getAdGroupAndIndexInMultiPeriodWindow(
|
||||||
|
oldPosition.periodIndex, adPlaybackState, timeline);
|
||||||
|
adGroupIndex = adGroupIndexAndAdIndexInAdGroup.first;
|
||||||
|
adIndexInAdGroup = adGroupIndexAndAdIndexInAdGroup.second;
|
||||||
|
}
|
||||||
|
int adState = adPlaybackState.getAdGroup(adGroupIndex).states[adIndexInAdGroup];
|
||||||
|
if (adState == AdPlaybackState.AD_STATE_AVAILABLE
|
||||||
|
|| adState == AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
||||||
|
setAdPlaybackState(
|
||||||
|
adPlaybackState.withPlayedAd(adGroupIndex, /* adIndexInAdGroup= */ adIndexInAdGroup));
|
||||||
}
|
}
|
||||||
setAdPlaybackState(newAdPlaybackState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,8 +711,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
|||||||
long positionInWindowUs =
|
long positionInWindowUs =
|
||||||
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
|
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
|
||||||
.positionInWindowUs;
|
.positionInWindowUs;
|
||||||
long currentPeriodPosition =
|
long currentPeriodPosition = msToUs(player.getCurrentPosition()) - positionInWindowUs;
|
||||||
Util.msToUs(player.getCurrentPosition()) - positionInWindowUs;
|
|
||||||
newAdPlaybackState =
|
newAdPlaybackState =
|
||||||
addLiveAdBreak(
|
addLiveAdBreak(
|
||||||
event.getAd(),
|
event.getAd(),
|
||||||
|
@ -23,6 +23,7 @@ import static java.lang.Math.max;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import androidx.annotation.CheckResult;
|
import androidx.annotation.CheckResult;
|
||||||
@ -355,14 +356,13 @@ import java.util.Set;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a
|
* Splits an {@link AdPlaybackState} into a separate {@link AdPlaybackState} for each period of a
|
||||||
* content timeline. Ad group times are expected to not take previous ad duration into account and
|
* content timeline.
|
||||||
* needs to be translated to the actual position in the {@code contentTimeline} by adding prior ad
|
|
||||||
* durations.
|
|
||||||
*
|
*
|
||||||
* <p>If a period is enclosed by an ad group, the period is considered an ad period and gets an ad
|
* <p>If a period is enclosed by an ad group, the period is considered an ad period. Splitting
|
||||||
* playback state assigned with a single ad in a single ad group. The duration of the ad is set to
|
* results in a separate {@link AdPlaybackState ad playback state} for each period that has either
|
||||||
* the duration of the period. All other periods are considered content periods with an empty ad
|
* no ads or a single ad. In the latter case, the duration of the single ad is set to the duration
|
||||||
* playback state without any ads.
|
* of the period consuming the entire duration of the period. Accordingly an ad period does not
|
||||||
|
* contribute to the duration of the containing window.
|
||||||
*
|
*
|
||||||
* @param adPlaybackState The ad playback state to be split.
|
* @param adPlaybackState The ad playback state to be split.
|
||||||
* @param contentTimeline The content timeline for each period of which to create an {@link
|
* @param contentTimeline The content timeline for each period of which to create an {@link
|
||||||
@ -398,15 +398,19 @@ import java.util.Set;
|
|||||||
long elapsedAdGroupAdDurationUs = 0;
|
long elapsedAdGroupAdDurationUs = 0;
|
||||||
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
|
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
|
||||||
contentTimeline.getPeriod(j, period, /* setIds= */ true);
|
contentTimeline.getPeriod(j, period, /* setIds= */ true);
|
||||||
if (totalElapsedContentDurationUs < adGroup.timeUs) {
|
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
|
||||||
|
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
|
||||||
|
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
|
||||||
// Period starts before the ad group, so it is a content period.
|
// Period starts before the ad group, so it is a content period.
|
||||||
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
|
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
|
||||||
totalElapsedContentDurationUs += period.durationUs;
|
totalElapsedContentDurationUs += period.durationUs;
|
||||||
} else {
|
} else {
|
||||||
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
|
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
|
||||||
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
|
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
|
||||||
// The period ends before the end of the ad group, so it is an ad period (Note: An ad
|
// Add one microsecond to work around rounding errors with adGroup.timeUs.
|
||||||
// reported by the IMA SDK may span multiple periods).
|
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
|
||||||
|
// The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad
|
||||||
|
// reported by the IMA SDK spans multiple periods before the LOADED event arrives).
|
||||||
adPlaybackStates.put(
|
adPlaybackStates.put(
|
||||||
checkNotNull(period.uid),
|
checkNotNull(period.uid),
|
||||||
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
|
splitAdGroupForPeriod(adsId, adGroup, periodStartUs, period.durationUs));
|
||||||
@ -430,7 +434,6 @@ import java.util.Set;
|
|||||||
|
|
||||||
private static AdPlaybackState splitAdGroupForPeriod(
|
private static AdPlaybackState splitAdGroupForPeriod(
|
||||||
Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) {
|
Object adsId, AdPlaybackState.AdGroup adGroup, long periodStartUs, long periodDurationUs) {
|
||||||
checkState(adGroup.timeUs <= periodStartUs);
|
|
||||||
AdPlaybackState adPlaybackState =
|
AdPlaybackState adPlaybackState =
|
||||||
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
|
new AdPlaybackState(checkNotNull(adsId), /* adGroupTimesUs...= */ 0)
|
||||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||||
@ -465,5 +468,54 @@ import java.util.Set;
|
|||||||
return adPlaybackState;
|
return adPlaybackState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@code adGroupIndex} and the {@code adIndexInAdGroup} for the given period index of
|
||||||
|
* an ad period.
|
||||||
|
*
|
||||||
|
* @param adPeriodIndex The period index of the ad period.
|
||||||
|
* @param adPlaybackState The ad playback state that holds the ad group and ad information.
|
||||||
|
* @param timeline The timeline that contains the ad period.
|
||||||
|
* @return A pair with the ad group index (first) and the ad index in that ad group (second).
|
||||||
|
*/
|
||||||
|
public static Pair<Integer, Integer> getAdGroupAndIndexInMultiPeriodWindow(
|
||||||
|
int adPeriodIndex, AdPlaybackState adPlaybackState, Timeline timeline) {
|
||||||
|
Timeline.Period period = new Timeline.Period();
|
||||||
|
int periodIndex = 0;
|
||||||
|
long totalElapsedContentDurationUs = 0;
|
||||||
|
for (int i = adPlaybackState.removedAdGroupCount; i < adPlaybackState.adGroupCount; i++) {
|
||||||
|
int adIndexInAdGroup = 0;
|
||||||
|
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(/* adGroupIndex= */ i);
|
||||||
|
long adGroupDurationUs = sum(adGroup.durationsUs);
|
||||||
|
long elapsedAdGroupAdDurationUs = 0;
|
||||||
|
for (int j = periodIndex; j < timeline.getPeriodCount(); j++) {
|
||||||
|
timeline.getPeriod(j, period, /* setIds= */ true);
|
||||||
|
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
|
||||||
|
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
|
||||||
|
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
|
||||||
|
// Period starts before the ad group, so it is a content period.
|
||||||
|
totalElapsedContentDurationUs += period.durationUs;
|
||||||
|
} else {
|
||||||
|
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
|
||||||
|
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
|
||||||
|
// Add one microsecond to work around rounding errors with adGroup.timeUs.
|
||||||
|
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
|
||||||
|
// The period ends before the end of the ad group, so it is an ad period.
|
||||||
|
if (j == adPeriodIndex) {
|
||||||
|
return new Pair<>(/* adGroupIndex= */ i, adIndexInAdGroup);
|
||||||
|
}
|
||||||
|
elapsedAdGroupAdDurationUs += period.durationUs;
|
||||||
|
adIndexInAdGroup++;
|
||||||
|
} else {
|
||||||
|
// Period is after the current ad group. Continue with next ad group.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Increment the period index to the next unclassified period.
|
||||||
|
periodIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
private ImaUtil() {}
|
private ImaUtil() {}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.ima;
|
package androidx.media3.exoplayer.ima;
|
||||||
|
|
||||||
|
import static androidx.media3.exoplayer.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
|
||||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
|
||||||
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
|
import static androidx.media3.test.utils.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;
|
||||||
@ -27,6 +28,7 @@ import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil;
|
|||||||
import androidx.media3.test.utils.FakeTimeline;
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@ -463,7 +465,9 @@ public class ImaUtilTest {
|
|||||||
AdPlaybackState adPlaybackState =
|
AdPlaybackState adPlaybackState =
|
||||||
new AdPlaybackState(
|
new AdPlaybackState(
|
||||||
/* adsId= */ "adsId",
|
/* adsId= */ "adsId",
|
||||||
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 1)
|
// TODO(b/192231683) Reduce additional period duration to 1 when rounding work
|
||||||
|
// around removed.
|
||||||
|
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs + 2)
|
||||||
.withAdCount(/* adGroupIndex= */ 0, 1)
|
.withAdCount(/* adGroupIndex= */ 0, 1)
|
||||||
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
|
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ periodDurationUs)
|
||||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||||
@ -724,4 +728,73 @@ public class ImaUtilTest {
|
|||||||
assertThat(adGroup.durationsUs[1]).isEqualTo(5_000_000);
|
assertThat(adGroup.durationsUs[1]).isEqualTo(5_000_000);
|
||||||
assertThat(adGroup.durationsUs[2]).isEqualTo(20_000_000);
|
assertThat(adGroup.durationsUs[2]).isEqualTo(20_000_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAdGroupAndIndexInMultiPeriodWindow_correctAdGroupIndexAndAdIndexInAdGroup() {
|
||||||
|
FakeTimeline timeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(/* periodCount= */ 9, new Object()));
|
||||||
|
long periodDurationUs = DEFAULT_WINDOW_DURATION_US / 9;
|
||||||
|
// [ad, ad, content, ad, ad, ad, content, ad, ad]
|
||||||
|
AdPlaybackState adPlaybackState =
|
||||||
|
new AdPlaybackState(/* adsId= */ "adsId", 0, periodDurationUs, 2 * periodDurationUs)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 0, 2)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 1, 3)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 2, 2)
|
||||||
|
.withAdDurationsUs(
|
||||||
|
/* adGroupIndex= */ 0,
|
||||||
|
DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US + periodDurationUs,
|
||||||
|
periodDurationUs)
|
||||||
|
.withAdDurationsUs(
|
||||||
|
/* adGroupIndex= */ 1, periodDurationUs, periodDurationUs, periodDurationUs)
|
||||||
|
.withAdDurationsUs(/* adGroupIndex= */ 2, periodDurationUs, periodDurationUs)
|
||||||
|
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||||
|
|
||||||
|
Pair<Integer, Integer> adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 0, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 1, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(0);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
|
||||||
|
|
||||||
|
Assert.assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(
|
||||||
|
/* adPeriodIndex= */ 2, adPlaybackState, timeline));
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 3, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 4, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 5, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(1);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(2);
|
||||||
|
|
||||||
|
Assert.assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() ->
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(
|
||||||
|
/* adPeriodIndex= */ 6, adPlaybackState, timeline));
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 7, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(0);
|
||||||
|
|
||||||
|
adGroupIndexAndAdIndexInAdGroup =
|
||||||
|
getAdGroupAndIndexInMultiPeriodWindow(/* adPeriodIndex= */ 8, adPlaybackState, timeline);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.first).isEqualTo(2);
|
||||||
|
assertThat(adGroupIndexAndAdIndexInAdGroup.second).isEqualTo(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user