/// <summary> /// This function attempts to parse the SCTE-35 payload, then schedule an appropriately offset Ad using the MediaBreak APIs /// </summary> /// <param name="timedMetadataTrack">Track which fired the cue</param> /// <param name="presentation_time_delta"></param> /// <param name="timescale"></param> /// <param name="event_duration"></param> /// <param name="scte35payload"></param> private async void ScheduleAdFromScte35Payload(TimedMetadataTrack timedMetadataTrack, uint presentation_time_delta, uint timescale, uint event_duration, string scte35payload) { // TODO: Parse SCTE-35 // // Ref: // http://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%20214-3%202015.pdf // // Eg: // // <SpliceInfoSection ptsAdjustment="0" scte35:tier="4095"> // <SpliceInsert spliceEventId="147467497" // spliceEventCancelIndicator="false" // outOfNetworkIndicator="false" // uniqueProgramId="0" // availNum="0" // availsExpected="0" // spliceImmediateFlag="false" > // <Program> // <SpliceTime ptsTime="6257853600"/> // </Program> // <BreakDuration autoReturn="true" duration="900000"/> // </SpliceInsert> // </SpliceInfoSection> // // // We use the metadata track object which fired the Cue to walk our way back up the // media object stack to find our original AdaptiveMediaSource. // // The AdaptiveMediaSource is required because the SCTE-35 payload includes // timing offsets which are relative to the original content PTS -- see below. var ams = timedMetadataTrack.PlaybackItem.Source.AdaptiveMediaSource; if (ams != null && timescale != 0) { // ++++ // NOTE: DO NOT PARSE SCTE35 LIKE THIS IN PRODUCTION CODE! // // Reminder: // // THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY // IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR // PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. // // We will not demonstrate proper SCTE35 parsing in this sample, // but instead we search through the xml find the ptsTime if present, // and use it to schedule an ad; keeping in place the various time offsets. // // e.g.: var sampleScte35Payload = "<SpliceInfoSection ptsAdjustment=\"0\" scte35:tier=\"4095\">< SpliceInsert spliceEventId = \"147467497\" spliceEventCancelIndicator = \"false\" outOfNetworkIndicator = \"false\" uniqueProgramId = \"0\" availNum = \"0\" availsExpected = \"0\" spliceImmediateFlag = \"false\" > < Program >< SpliceTime ptsTime = \"6257853600\" /></ Program > < BreakDuration autoReturn = \"true\" duration = \"900000\" /> </ SpliceInsert > </ SpliceInfoSection > "; // var xmlStrings = scte35payload.Split(new string[] { "<", "/", ">", " ", "=", "\"" }, StringSplitOptions.RemoveEmptyEntries); string ptsTime = string.Empty; for (int i = 0; i < xmlStrings.Length; i++) { if (xmlStrings[i] == "ptsTime") { if (i + 1 < xmlStrings.Length) { ptsTime = xmlStrings[i + 1]; break; } } } // ---- // The AdaptiveMediaSource keeps track of the original PTS in an AdaptiveMediaSourceCorrelatedTimes // object that can be retrieved via ams.GetCorrelatedTimes(). All the while, it provids the // media pipeline with a consistent timeline that re-aligns zero at the begining of the SeekableRange // when first joining a Live stream. long pts = 0; long.TryParse(ptsTime, out pts); var timeCorrelation = ams.GetCorrelatedTimes(); if (timeCorrelation.Position.HasValue && timeCorrelation.PresentationTimeStamp.HasValue) { TimeSpan currentPosition; await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { currentPosition = mediaPlayerElement.MediaPlayer.PlaybackSession.Position; }); TimeSpan emsgPresentationTimeDelta = TimeSpan.FromSeconds(presentation_time_delta / timescale); long delayInTicks = timeCorrelation.PresentationTimeStamp.Value.Ticks - pts; TimeSpan correctionForAsyncCalls = currentPosition - timeCorrelation.Position.Value; TimeSpan targetAdPosition = emsgPresentationTimeDelta + TimeSpan.FromTicks(delayInTicks) + correctionForAsyncCalls; Log($"Timing Info: PlaybackSession.Position:{currentPosition} targetAdPosition:{targetAdPosition} Delta:{targetAdPosition-currentPosition} Ams.Position:{timeCorrelation.Position.GetValueOrDefault().Ticks} SCTE ptsTime:{pts} emsgPresentationTimeDelta:{emsgPresentationTimeDelta} Ams.PresentationTimeStamp:{timeCorrelation.PresentationTimeStamp.GetValueOrDefault().Ticks} Ams.ProgramDateTime:{timeCorrelation.ProgramDateTime.GetValueOrDefault()}"); MediaBreakInsertionMethod insertionMethod = ams.IsLive ? MediaBreakInsertionMethod.Replace : MediaBreakInsertionMethod.Interrupt; var redSkyUri = new Uri("http://az29176.vo.msecnd.net/videocontent/RedSky_FoxRiverWisc_GettyImagesRF-499617760_1080_HD_EN-US.mp4"); if (targetAdPosition != TimeSpan.Zero && targetAdPosition >= currentPosition) { // Schedule ad in the future: Log($"Ad insertion triggerd by 'emsg'. Scheduled: {targetAdPosition.ToString()} Current:{currentPosition.ToString()}"); var midRollBreak = new MediaBreak(insertionMethod, targetAdPosition); midRollBreak.PlaybackList.Items.Add(new MediaPlaybackItem(MediaSource.CreateFromUri(redSkyUri))); timedMetadataTrack.PlaybackItem.BreakSchedule.InsertMidrollBreak(midRollBreak); } else { // Play now! Log($"Ad inserted immediately. Scheduled: {targetAdPosition.ToString()} Current:{currentPosition.ToString()}"); var midRollBreak = new MediaBreak(insertionMethod); midRollBreak.PlaybackList.Items.Add(new MediaPlaybackItem(MediaSource.CreateFromUri(redSkyUri))); await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { mediaPlayerElement.MediaPlayer.BreakManager.PlayBreak(midRollBreak); }); } } } }
private void CreateMediaBreaksForItem(MediaPlaybackItem item) { if (item != null) { // We have two ads that will be repeated. var redSkyUri = new Uri("http://az29176.vo.msecnd.net/videocontent/RedSky_FoxRiverWisc_GettyImagesRF-499617760_1080_HD_EN-US.mp4"); var flowersUri = new Uri("http://az29176.vo.msecnd.net/videocontent/CrocusTL_FramePoolRM_688-580-676_1080_HD_EN-US.mp4"); // One option is to create a separate MediaPlaybackItem for each of your ads. // You might choose to do this if each ad needs different reporting information. // Another option is to re-use MediaPlaybackItems in different MediaBreaks. // This scenario demonstrates both patterns. var ad1 = new MediaPlaybackItem(MediaSource.CreateFromUri(redSkyUri)); ad1.Source.CustomProperties["contentId"] = "Ad1_ID"; ad1.Source.CustomProperties["description"] = "Red Sky"; ad1.Source.CustomProperties["uri"] = redSkyUri.ToString(); RegisterForMediaSourceEvents(ad1.Source); RegisterForMediaPlaybackItemEvents(ad1); var ad2 = new MediaPlaybackItem(MediaSource.CreateFromUri(flowersUri)); ad2.Source.CustomProperties["contentId"] = "Ad2_ID"; ad2.Source.CustomProperties["description"] = "Flowers"; ad2.Source.CustomProperties["uri"] = flowersUri.ToString(); RegisterForMediaSourceEvents(ad2.Source); RegisterForMediaPlaybackItemEvents(ad2); var ad3 = new MediaPlaybackItem(MediaSource.CreateFromUri(redSkyUri)); ad3.Source.CustomProperties["contentId"] = "Ad3_ID"; ad3.Source.CustomProperties["description"] = "Red Sky 2"; ad3.Source.CustomProperties["uri"] = redSkyUri.ToString(); RegisterForMediaSourceEvents(ad3.Source); RegisterForMediaPlaybackItemEvents(ad3); // Create a PrerollBreak on your main content. if (item.BreakSchedule.PrerollBreak == null) { item.BreakSchedule.PrerollBreak = new MediaBreak(MediaBreakInsertionMethod.Interrupt); } // Add the ads to the PrerollBreak in the order you want them played item.BreakSchedule.PrerollBreak.PlaybackList.Items.Add(ad1); item.BreakSchedule.PrerollBreak.PlaybackList.Items.Add(ad2); // Add the ads to the MidRoll break at 10% into the main content. // To do this, we need to wait until the main MediaPlaybackItem is fully loaded by the player // so that we know its Duration. This will happen on MediaSource.OpenOperationCompleted. item.Source.OpenOperationCompleted += (sender, args) => { var attachedItem = MediaPlaybackItem.FindFromMediaSource(sender); if (sender.Duration.HasValue) { // For live streaming, the duration will be TimeSpan.MaxValue, which won't work for this scenario, // so we'll assume the total duration is 2 minutes for the purpose of ad insertion. bool isLiveMediaSource = item.Source.AdaptiveMediaSource != null ? item.Source.AdaptiveMediaSource.IsLive : false; long sourceDurationTicks = isLiveMediaSource ? TimeSpan.FromMinutes(2).Ticks : sender.Duration.Value.Ticks; var positionAt10PercentOfMainContent = TimeSpan.FromTicks(sourceDurationTicks / 10); // If the content is live, then the ad break replaces the streaming content. // If the content is not live, then the content pauses for the ad, and then resumes // after the ad is complete. MediaBreakInsertionMethod insertionMethod = isLiveMediaSource ? MediaBreakInsertionMethod.Replace : MediaBreakInsertionMethod.Interrupt; var midRollBreak = new MediaBreak(insertionMethod, positionAt10PercentOfMainContent); midRollBreak.PlaybackList.Items.Add(ad2); midRollBreak.PlaybackList.Items.Add(ad1); attachedItem.BreakSchedule.InsertMidrollBreak(midRollBreak); Log($"Added MidRoll at {positionAt10PercentOfMainContent}"); } }; // Create a PostrollBreak: // Note: for Live content, it will only play once the presentation transitions to VOD. if (item.BreakSchedule.PostrollBreak == null) { item.BreakSchedule.PostrollBreak = new MediaBreak(MediaBreakInsertionMethod.Interrupt); } // Add the ads to the PostrollBreak in the order you want them played item.BreakSchedule.PostrollBreak.PlaybackList.Items.Add(ad3); } }