Implement blackout handling

TVSDK provides APIs and sample code for handling blackout periods.

To implement blackout handling including providing alternate content during the blackout:

  1. Set up your app to detect blackout tags in a live stream manifest.

    public void createMediaPlayer {
        ...
        String[] blackoutTags = {BLACKOUTTAG};
        PSDKConfig.setSubscribedTags(blackoutTags);
        // For example: PTSDKConfig.setSubscribedTags({"#EXT-OATCLS-SCTE35"});
    }
    
  2. Create event listeners for timed metadata events in foreground and background streams.

    private MediaPlayer createMediaPlayer() {
        mediaPlayer.addEventListener(MediaPlayer.Event.PLAYBACK, _playbackEventListener);
        mediaPlayer.addEventListener(MediaPlayer.Event.BLACKOUTS, _blackoutsEventListener);
    }
    
  3. Implement timed metadata event handlers for both foreground and background streams.

    Foreground:

    private final MediaPlayer.PlaybackEventListener _playbackEventListener =
              new MediaPlayer.PlaybackEventListener() {
        ...
    
        @override
        public void onTimedMetadata(TimedMetadata timedMetadata) {
            if (timedMetadata.getName().equal(BLACKOUTTAG) &&
                !_timedMetadataList.contains(timedMetadata)) {
                  _timedMetadataList.add(timedMetadata);
            }
        }
        ...
    }
    
    private final MediaPlayer.BlackoutsEventListener _blackoutsEventListener =
      new MediaPlayer.BlackoutsEventListener() {
        @Override
        public void onTimedMetadataInBackgroundItem(TimedMetadata timedMetadata) {
            TimedMetadata.Type type = timedMetadata.getType();
            if (type.equals(TimedMetadata.Type.TAG) && _mediaPlayer.getPlaybackRange() != null
                && _mediaPlayer.getPlaybackRange().getDuration() > 0) {
                if (!_timedMetadataList.contains(timedMetadata) && isBlackoutMetadata(timedMetadata)) {
                    _timedMetadataList.add(timedMetadata);
                }
            }
        }
    
        @Override
        public void onBackgroundManifestFailed() {
            ...
        }
    };
    
    
  4. Handle TimedMetadata objects when MediaPlayer time runs.

    _playbackClockEventListener = new Clock.ClockEventListener() {
        @Override
        public void onTick(String name) {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    /* handle timedmetadata object list  */
                    if (_mediaPlayer != null && _timedMetadataList != null
                        && _timedMetadataList.size() > 0) {
                        if (_lastKnownStatus == MediaPlayer.PlayerState.PLAYING) {
                            long localTime = _mediaPlayer.getLocalTime();
                            handleTimedMetadataList(localTime);
                        }
                    }
                }
            });
        }
    };
    
  5. Create methods for switching content at the start and end of the blackout period.

    private void handleTimedMetadataList(long currentTime) {
        for (int i = 0; i < _timedMetadataList.size(); i++) {
            TimedMetadata timedMetadata = _timedMetadataList.get(i);
            long diff = localTime - timedMetadata.getTime();
            if (!_timedMetadataDispatchedList.contains(timedMetadata)
                && diff >= 0
                && diff <= PLAYBACK_CLOCK_INTERVAL
                && _mediaPlayer.shouldTriggerSubscribedTagEvent()) {
                    // switch to blackout content
                if (!_inBlackout && isBlackoutStartTimedMetadata(timedMetadata)) {
                    MediaResource blackoutMediaResource = createBlackoutMediaResource(timedMetadata);
    
                    //1. register current item as background item
                    _mediaPlayer.registerCurrentItemAsBackgroundItem();
    
                    //2. replace current item with blackout item
                    _mediaPlayer.replaceCurrentItem(blackoutMediaResource);
    
                    //3. update qos metrics
                    _mediaQosProvider.updateMetrics(_mediaPlayer);
    
                    //4. maintain state
                    _inBlackout = true;
                    resetTimedMetada();
    
                    break;
                }
                // switch back to main content
                else if (_inBlackout && isBlackoutEndTimedMetadata(timedMetadata)) {
                    //1. register current item as background item
                    _mediaPlayer.unregisterCurrentBackgroundItem();
    
                    //2. replace current item with blackout item
                    _mediaPlayer.replaceCurrentItem(_oldMediaResource);
    
                    //3. update qos metrics
                    _mediaQosProvider.updateMetrics(_mediaPlayer);
    
                    //4. maintain state
                    _inBlackout = false;
                    resetTimedMetada();
    
                    break;
                }
            }
        }
    }
    
  6. Update non seekable ranges if the blackout range is in DVR on the playback stream.

    // prepare and update blackout nonSeekable ranges
    
    List<TimeRange> blackoutRanges = prepareBlackoutRanges(_timedMetadataList);
    if (blackoutRanges != null && blackoutRanges.size() > 0) {
        int size = blackoutRanges.size();
        TimeRange[] blackoutRangesArray = blackoutRanges.toArray(new TimeRange[size]);
        BlackoutMetadata blackoutMetadata = new BlackoutMetadata(blackoutRangesArray);
        updateBlackoutMetadata(blackoutMetadata);
    }
    
    // function to update blackout metadata
    private void updateBlackoutMetadata(BlackoutMetadata blackoutMetadata) {
        MediaPlayerItem currentItem = _mediaPlayer.getCurrentItem();
        if (currentItem != null) {
            Metadata metadata = currentItem.getResource().getMetadata();
            if (metadata != null) {
                MetadataNode metadataNode = ((MetadataNode) metadata);
                metadataNode.setNode(DefaultMetadataKeys.BLACKOUT_METADATA_KEY.getValue(),
                                     blackoutMetadata);
    
                for (int i = 0; i < blackoutMetadata.getNonSeekableRanges().length; i++) {
                    TimeRange timeRange = blackoutMetadata.getNonSeekableRanges()[i];
                }
            }
        }
    }
    
    NOTE

    Currently for multiple bit-rate live streams, occasionally the adjustable bit-rate (ABR) profiles can get out of sync. This causes duplicate timedMetadata objects for the same subscribed tag. To avoid incorrect non-seekable calculations, it is highly recommended to check for overlapping non-seekable ranges after your calculations, such as in the following example:

    List<TimeRange> rangesToRemove = new ArrayList<TimeRange>();
    
    for (int i = 0; i < nonSeekableRanges.size() - 1; i++) {
        TimeRange range1 = nonSeekableRanges.get(i);
        TimeRange range2 = nonSeekableRanges.get(i + 1);
        if (range1.contains(range2.getBegin()) && !rangesToRemove.contains(range2)) {
           rangesToRemove.add(range2);
        } else if (range2.contains(range1.getBegin()) && !rangesToRemove.contains(range1)) {
           rangesToRemove.add(range1);
       }
    }
    
    if (nonSeekableRanges.size() > 0 && rangesToRemove.size() > 0) {
        nonSeekableRanges.removeAll(rangesToRemove);
        for (int i = 0; i < rangesToRemove.size(); i++) {
            TimeRange range = rangesToRemove.get(i);
        }
    }
    
    if (nonSeekableRanges.size() > 0 && rangesToRemove.size() > 0) {
        nonSeekableRanges.removeAll(rangesToRemove);
    }
    

On this page