The TVSDK provides APIs and sample code for handling blackout periods.
To implement blackout handling and provide alternate content during the blackout:
Set up your app to subscribe to blackout tags in a live stream manifest.
- (void) createMediaPlayer:(PTMediaPlayerItem *)item
{
[PTSDKConfig setSubscribedTags:[NSArray arrayWithObject:<INSERT-BLACKOUT-TAG>]];
// For example:
// [PTSDKConfig setSubscribedTags:[NSArray arrayWithObject:@"#EXT-OATCLS-SCTE35"]];
}
Add a notification listener for PTTimedMetadataChangedNotification
.
- (void)addobservers
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaPlayerSubscribedTagIdentified:)
name:PTTimedMetadataChangedNotification object:self.player.currentItem];
}
Implement a listener method for PTTimedMetadata
objects in foreground.
For example:
- (void)onMediaPlayerSubscribedTagIdentified:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo];
PTTimedMetadata *timedMetadata = [(PTTimedMetadata *)[userInfo objectForKey:PTTimedMetadataKey]
retain];
if ([timedMetadata.name isEqualToString:<INSERT-BLACKOUT-TAG>])
{
// handle tag. For example: store it in a dictionary keyed by time to be handled when
// playback time = timedMetadata time.
NSNumber *timedMetadataStartTime =
[NSNumber numberWithInt:(int)CMTimeGetSeconds(timedMetadata.time)];
[timedMetadataCollection setObject:timedMetadata forKey:timedMetadataStartTime];
}
[timedMetadata release];
}
Handle TimedMetadata
objects with constant updates during playback.
- (void)onMediaPlayerTimeChange:(NSNotification *)notification
{
@synchronized(self)
{
CMTimeRange seekableRange = self.player.seekableRange;
if (CMTIMERANGE_IS_VALID(seekableRange))
{
_currentTime = (int) CMTimeGetSeconds(self.player.currentTime);
if (isnan(_currentTime))
{
_currentTime = 0;
}
[self handleCollectionAtTime:_currentTime];
}
}
}
Add the PTTimedMetadata
handler to switch to alternate content and return to main content as indicated by the PTTimedMetadata
object and its playback time.
- (void)handleCollectionAtTime:(int)currentTime
{
NSArray *allKeys = nil;
NSMutableArray * timedMetadatasToDelete = [[[NSMutableArray alloc]init]autorelease];
if (!_inBlackout && timedMetadataCollection)
{
allKeys = [timedMetadataCollection allKeys];
int count = [allKeys count];
for (int i=count-1; i>-1; i--)
{
NSNumber *currTimedMetadataTime = allKeys[i];
PTTimedMetadata *currTimedMetadata =
[timedMetadataCollection objectForKey:currTimedMetadataTime];
if (currentTime == [currTimedMetadataTime integerValue] &&
currTimedMetadata.name == <INSERT-BLACKOUT-TAG> &&
[self isBlackoutStart: currTimedMetadata])
{
PTAdMetadata *newItemAdMetadata =
[self createAlternateMediaMetadata];
// 1. Turn off preroll on the alternate media item.
newItemAdMetadata.enableLivePreroll = NO;
PTMediaPlayerItem *newItem =
[[PTMediaPlayerItem alloc]initWithUrl:
<INSERT-ALTERNATE-STREAM-URL mediaId:<INSERT-ALTERNATE-STREAM-
MEDIA-ID> metadata:newItemAdMetadata];
// 2. Register the current (original playback item) in background.
[self.player registerCurrentItemAsBackgroundItem];
// 3. Replace the current playback item with the alternate stream.
[self.player replaceCurrentItemWithPlayerItem:newItem];
// 4. Reset observers.
[self removeObservers];
[self addobservers];
// 5. Register listener on the subscribed tags in background item.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onSubscribedTagInBackground:)
name:PTTimedMetadataChangedInBackgroundNotification
object:self.player.currentItem];
// 6. Register listener on the error in background item.
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(onBackgroundManifestError:)
name:PTBackgroundManifestErrorNotification
object:self.player.currentItem];
// 7. Resume playback
[self.player play];
// 8. Set boolean to true to handle blackout end.
_inBlackout = YES;
break;
}
}
}
else if (_inBlackout && backgroundTimedMetadataCollection)
{
allKeys = [backgroundTimedMetadataCollection allKeys];
int count = [allKeys count];
for (int i=count-1; i>-1; i--)
{
NSNumber *currTimedMetadataTime = allKeys[i];
PTTimedMetadata *currTimedMetadata =
[backgroundTimedMetadataCollection objectForKey:allKeys[i]];
if (currentTime == ([currTimedMetadataTime integerValue] &&
currTimedMetadata.name == <INSERT-BLACKOUT-TAG> &&
[self isBlackoutEnd:currTimedMetadata] )
{
// 1. Come out of blackout. Unregister background item.
[self.player unregisterCurrenBackgroundItem];
PTMetadata *metadata = [self createMetadata];
PTAdMetadata *adMetadata =
(PTAdMetadata *)[currMetadata metadataForKey:PTAdResolvingMetadataKey];
adMetadata.enableLivePreroll = NO;
PTMediaPlayerItem *item =
[[[PTMediaPlayerItem alloc] initWithUrl:<INSERT-ORIGINAL-URL>
mediaId:<INSERT-ORIGINAL-MEDIAID> metadata:metadata autorelease];
// 2. Switch back to original item.
[self.player replaceCurrentItemWithPlayerItem:item];
self.player.autoPlay = YES;
[self removeObservers];
// 3. Remove background item listener.
[[NSNotificationCenter defaultCenter] removeObserver:self
name:PTTimedMetadataChangedInBackgroundNotification
object:self.player.currentItem];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:PTBackgroundManifestErrorNotification
object:self.player.currentItem];
[self addobservers];
[self.player play];
// 4. Update boolean to correctly maintain the current state.
_inBlackout = NO;
break;
}
}
}
}
Implement a listener method for PTTimedMetadata
objects in the background.
- (void)onSubscribedTagInBackground:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo];
PTTimedMetadata *timedMetadata = [(PTTimedMetadata *)
[userInfo objectForKey:PTTimedMetadataKey] retain];
if ([timedMetadata.name isEqualToString:<INSERT-BLACKOUT-TAG>])
{
NSNumber *timedMetadataStartTime =
[NSNumber numberWithInt:(int)CMTimeGetSeconds(timedMetadata.time)];
[backgroundTimedMetadataCollection
setObject:timedMetadata forKey:timedMetadataStartTime];
}
[timedMetadata release];
}
Implement a listener method for background errors.
- (void) onBackgroundManifestError:(NSNotification *)notification
{
NSLog (@"onBackgroundManifestError");
}
If the blackout range is on the DVR in the playback stream, update the non-seekable ranges.
// This sample assumes that blackoutStartTimedMetadata is the PTTimedMetadata
// object that indicated "blackout start", and assuming blackoutEndTimedMetadata is the
// PTTimedMetadataObject that indicated "blackout end". Since in this case they are both
// in DVR, both are notified to the application before playback starts. This is the right
// time for the application to set this range in DVR as non-seekable range.
CMTime ignoreRangeStart = blackoutStartTimedMetadata.time;
CMTime ignoreRangeDuration = CMTimeMakeWithSeconds(CMTimeMakeWithSeconds
(CMTimeGetSeconds(blackoutEndTimedMetadata.time) -
CMTimeGetSeconds(blackoutStartTimedMetadata.time)),
blackoutEndTimedMetadata.time.timescale);
CMTimeRange ignoreRange = CMTimeRangeMake(ignoreRangeStart, ignoreRangeDuration);
NSArray *ignoreRangeArray = [NSArray arrayWithObject:[NSValue valueWithCMTimeRange:ignoreRange]];
PTBlackoutMetadata *blackoutMetadata =
[[PTBlackoutMetadata alloc]initWithNonSeekableRanges:ignoreRangeArray];
PTMetadata *currMetadata = self.item.metadata;
if (currMetadata)
{
[currMetadata setMetadata:blackoutMetadata forKey:PTBlackoutMetadataKey]
}