Return to a skipped ad break

  • Snapback is a feature that prevents viewers from seeking past mid-roll ads by returning them to the start of an ad break if they attempt to do so.

  • After the ad break completes, snapback then returns the viewer to their original desired seek location.

  • Implementing snapback involves checking if a seek skipped an unwatched ad break and, if so, seeking back to the ad break start, followed by returning the user to their original seek point after the ad break finishes.

Select platform: HTML5 Android iOS tvOS Roku

As a video publisher, you may want to prevent your viewers from seeking past your mid-roll ads. When a user seeks past an ad break, you can take them back to the start of that ad break, and then return them to their seek location after that ad break has completed. This feature is called "snapback."

As an example, see the diagram below. Your viewer is watching a video, and decides to seek from the 5-minute mark to the 15-minute mark. There is, however, an ad break at the 10-minute mark that you want them to watch before they can watch the content after it:

In order to show this ad break, take the following steps:

  1. Check if the user ran a seek that jumped past an unwatched ad break, and if so, take them back to the ad break.
  2. After the ad break completes, return them to their original seek.

In diagram form, that looks like this:

Here's how to implement this workflow in the IMA DAI SDK, as done in the AdvancedExample.

Prevent skipping unwatched ads

If a user attempts to skip over an ad break, the player must detect the jump and force playback to the start of that specific ad break. To prevent skipping unwatched ads, do the following:

  1. When the user begins interacting with the seek bar, record their current playback time.
  2. After the user finishes seeking to a different time in the stream, identify the most recent ad break located before this time.
  3. If the ad break starts after the recorded start time, indicating a skip, and has not yet been played, seek the player to the beginning of the ad break.
  4. Enable a snapbackMode flag to track that this ad break was forced.

Objective-C

- (IBAction)videoControlsTouchStarted:(id)sender {
  [NSObject cancelPreviousPerformRequestsWithTarget:self
                                            selector:@selector(hideFullscreenControls)
                                              object:self];

  self.currentlySeeking = YES;
  self.seekStartTime = self.contentPlayer.currentTime;
}

- (IBAction)videoControlsTouchEnded:(id)sender {
  if (self.fullscreen) {
    [self startHideControlsTimer];
  }
  self.currentlySeeking = NO;
  if (!self.adPlaying) {
    self.seekEndTime = CMTimeMake(self.progressBar.value, 1);
    IMACuepoint *lastCuepoint =
        [self.streamManager previousCuepointForStreamTime:CMTimeGetSeconds(self.seekEndTime)];
    if (!lastCuepoint.played && (lastCuepoint.startTime > CMTimeGetSeconds(self.seekStartTime))) {
      self.snapbackMode = YES;
      // Add 1 to the seek time to get the keyframe at the start of the ad to be our landing
      // place.
      [self.contentPlayer
          seekToTime:CMTimeMakeWithSeconds(lastCuepoint.startTime + 1, NSEC_PER_SEC)];
    }
  }
}

Swift

@IBAction func progressBarTouchStarted(_ sender: UISlider) {
  guard !isAdPlaying else { return }
  currentlySeeking = true
  seekStartTime = contentPlayer.currentTime().seconds
}

// MARK: Snapback Logic
@IBAction func progressBarTouchEnded(_ sender: UISlider) {
  guard !isAdPlaying else { return }
  if isFullScreen {
    startHideControlsTimer()
  }
  currentlySeeking = false
  seekEndTime = Float64(sender.value)

  guard let streamManager else { return }

  if let lastCuepoint = streamManager.previousCuepoint(forStreamTime: seekEndTime) {
    if !lastCuepoint.isPlayed, lastCuepoint.startTime > seekStartTime {
      logMessage(
        "Snapback to \(String(format: "%.2f", lastCuepoint.startTime)) from \(String(format: "%.2f", seekEndTime))"
      )
      snapbackMode = true
      contentPlayer.seek(
        to: CMTime(seconds: Double(sender.value), preferredTimescale: 1000))
    }
  }
}

Resume the original seek

After the forced ad break has finished playing, the player brings the user to the intended content point.

To resume the user's original seek, do the following:

  1. Listen for the AD_BREAK_ENDED event in your stream manager.

  2. Check if the snapbackMode flag is active to make sure this jump happens after a forced ad view.

  3. If active, seek the player to the saved destination time to return the user to the intended timestamp.

The following example listens for a finished ad break and returns a user to the original seek:

Objective-C

case kIMAAdEvent_AD_BREAK_ENDED: {
  [self logMessage:@"Ad break ended"];
  self.adPlaying = NO;
  if (self.snapbackMode) {
    self.snapbackMode = NO;
    if (CMTimeCompare(self.seekEndTime, self.contentPlayer.currentTime)) {
      [self.contentPlayer seekToTime:self.seekEndTime];
    }
  }
  break;
}

Swift

case .AD_BREAK_ENDED:
  logMessage("Ad break ended")
  isAdPlaying = false
  progressBar.isUserInteractionEnabled = true
  if snapbackMode {
    snapbackMode = false
    if contentPlayer.currentTime().seconds < seekEndTime {
      contentPlayer.seek(to: CMTime(seconds: Double(seekEndTime), preferredTimescale: 1000))
    }
  } else if pendingBookmarkSeek, let time = bookmarkStreamTime {
    logMessage(String(format: "AD_BREAK_ENDED: Seeking to bookmark streamTime: %.2f", time))
    imaVideoDisplay.seekStream(toTime: time)
    pendingBookmarkSeek = false
    bookmarkStreamTime = nil
  }
  updatePlayHeadState(isPlaying: self.isContentPlaying)