mlbserver 2022.3.27 → 2022.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/index.js +113 -91
  3. package/package.json +1 -1
  4. package/session.js +120 -134
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mlbserver
2
2
 
3
- Current version 2022.03.27
3
+ Current version 2022.05.09
4
4
 
5
5
  Credit to https://github.com/tonycpsu/streamglob and https://github.com/mafintosh/hls-decryptor
6
6
 
package/index.js CHANGED
@@ -25,6 +25,7 @@ const YESTERDAY_UTC_HOURS = 14 // UTC hours (EST + 4) to change home page defaul
25
25
  const VALID_MEDIA_TYPES = [ 'Video', 'Audio', 'Spanish' ]
26
26
  const VALID_LINK_TYPES = [ 'Embed', 'Stream', 'Chromecast', 'Advanced' ]
27
27
  const VALID_START_FROM = [ 'Beginning', 'Live' ]
28
+ const VALID_CONTROLS = [ 'Show', 'Hide' ]
28
29
  const VALID_INNING_HALF = [ '', 'top', 'bottom' ]
29
30
  const VALID_INNING_NUMBER = [ '', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
30
31
  const VALID_SCORES = [ 'Hide', 'Show' ]
@@ -34,7 +35,8 @@ const DEFAULT_MULTIVIEW_RESOLUTION = '540p'
34
35
  const VALID_BANDWIDTHS = [ '', '6600k', '4160k', '2950k', '2120k', '1400k', '' ]
35
36
  const VALID_AUDIO_TRACKS = [ 'all', 'English', 'English Radio', 'Radio Española', 'none' ]
36
37
  const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
37
- const VALID_SKIP = [ 'off', 'breaks', 'pitches' ]
38
+ const VALID_SKIP = [ 'off', 'breaks', 'idle time', 'pitches' ]
39
+ const VALID_PAD = [ 'off', 'on' ]
38
40
  const VALID_FORCE_VOD = [ 'off', 'on' ]
39
41
 
40
42
  const SAMPLE_STREAM_URL = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'
@@ -133,7 +135,7 @@ session.setPorts(port, multiview_port)
133
135
  app.listen(port, function(addr) {
134
136
  session.log(appname + ' started at http://' + addr)
135
137
  session.debuglog('multiview port ' + multiview_port)
136
- session.log('multiview server started at http://' + addr.replace(':' + port, ':' + multiview_port) + multiview_url_path)
138
+ session.debuglog('multiview server started at http://' + addr.replace(':' + port, ':' + multiview_port) + multiview_url_path)
137
139
  session.clear_multiview_files()
138
140
  })
139
141
  var multiview_app = http.createServer()
@@ -179,6 +181,11 @@ app.get('/stream.m3u8', async function(req, res) {
179
181
  options.inning_half = req.query.inning_half || VALID_INNING_HALF[0]
180
182
  options.inning_number = req.query.inning_number || VALID_INNING_NUMBER[0]
181
183
  options.skip = req.query.skip || VALID_SKIP[0]
184
+ options.pad = req.query.pad || VALID_PAD[0]
185
+ if ( options.pad != VALID_PAD[0] ) {
186
+ // if pad is selected, pick a random number of times to repeat the last segment
187
+ options.pad = Math.floor(Math.random() * 1440) + 720
188
+ }
182
189
 
183
190
  if ( req.query.src ) {
184
191
  streamURL = req.query.src
@@ -231,15 +238,8 @@ app.get('/stream.m3u8', async function(req, res) {
231
238
  if ( contentId ) {
232
239
  options.contentId = contentId
233
240
 
234
- let skip_adjust = req.query.skip_adjust || 0
235
- let skip_types = []
236
- if ( (options.inning_half != VALID_INNING_HALF[0]) || (options.inning_number != VALID_INNING_NUMBER[0]) ) {
237
- skip_types.push('innings')
238
- }
239
- if ( options.skip != VALID_SKIP[0] ) {
240
- skip_types.push(options.skip)
241
- }
242
- await session.getEventOffsets(contentId, skip_types, skip_adjust)
241
+ let skip_type = VALID_SKIP.indexOf(options.skip)
242
+ await session.getSkipMarkers(contentId, skip_type, options.inning_number, options.inning_half)
243
243
  }
244
244
  }
245
245
 
@@ -364,6 +364,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
364
364
  let inning_half = options.inning_half || VALID_INNING_HALF[0]
365
365
  let inning_number = options.inning_number || VALID_INNING_NUMBER[0]
366
366
  let skip = options.skip || VALID_SKIP[0]
367
+ let pad = options.pad || VALID_PAD[0]
367
368
  let contentId = options.contentId || false
368
369
 
369
370
  if ( (inning_number > 0) && (inning_half == VALID_INNING_HALF[0]) ) {
@@ -444,6 +445,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
444
445
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
445
446
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
446
447
  if ( skip != VALID_SKIP[0] ) newurl += '&skip=' + skip
448
+ if ( pad != VALID_PAD[0] ) newurl += '&pad=' + pad
447
449
  if ( contentId ) newurl += '&contentId=' + contentId
448
450
  newurl += content_protect + referer_parameter
449
451
  if ( resolution == 'none' ) {
@@ -490,6 +492,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
490
492
  if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
491
493
  if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
492
494
  if ( skip != VALID_SKIP[0] ) newurl += '&skip=' + skip
495
+ if ( pad != VALID_PAD[0] ) newurl += '&pad=' + pad
493
496
  if ( contentId ) newurl += '&contentId=' + contentId
494
497
  newurl += content_protect + referer_parameter
495
498
  return 'playlist?url='+newurl
@@ -537,6 +540,7 @@ app.get('/playlist', async function(req, res) {
537
540
  var inning_half = req.query.inning_half || VALID_INNING_HALF[0]
538
541
  var inning_number = req.query.inning_number || VALID_INNING_NUMBER[0]
539
542
  var skip = req.query.skip || VALID_SKIP[0]
543
+ var pad = req.query.pad || VALID_PAD[0]
540
544
  var contentId = req.query.contentId || false
541
545
 
542
546
  var req = function () {
@@ -574,26 +578,22 @@ app.get('/playlist', async function(req, res) {
574
578
 
575
579
  var key
576
580
  var iv
581
+ var skip_markers
582
+ var skip_marker_index = 0
583
+ var time_counter = 0.0
584
+ var skip_next = false
585
+ var discontinuity = false
577
586
 
578
587
  var content_protect = ''
579
588
  if ( session.protection.content_protect ) {
580
589
  content_protect = '&content_protect=' + session.protection.content_protect
581
590
  }
582
591
 
583
- if ( (contentId) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0]))) {
584
- // If inning offsets don't exist, we'll force those options off
585
- if ( (typeof session.temp_cache[contentId] === 'undefined') || (typeof session.temp_cache[contentId].inning_offsets === 'undefined') ) {
586
- inning_half = VALID_INNING_HALF[0]
587
- inning_number = VALID_INNING_NUMBER[0]
588
- skip = 'off'
589
- } else {
590
- var time_counter = 0.0
591
- var skip_index = 1
592
- var skip_next = false
593
- var discontinuity = false
594
-
595
- var offsets = session.temp_cache[contentId].event_offsets
596
- }
592
+ if ( (contentId) && ((inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0])) && (typeof session.temp_cache[contentId] !== 'undefined') && (typeof session.temp_cache[contentId].skip_markers !== 'undefined') ) {
593
+ session.debuglog('pulling skip markers from temporary cache')
594
+ skip_markers = session.temp_cache[contentId].skip_markers
595
+ } else {
596
+ session.debuglog('not using skip markers from temporary cache')
597
597
  }
598
598
 
599
599
  body = body
@@ -601,7 +601,7 @@ app.get('/playlist', async function(req, res) {
601
601
  // Skip blank lines
602
602
  if (line.trim() == '') return null
603
603
 
604
- if ( ((skip != 'off') || (inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0])) && (typeof session.temp_cache[contentId] !== 'undefined') && (typeof session.temp_cache[contentId].inning_offsets !== 'undefined') ) {
604
+ if ( skip_markers && skip_markers[skip_marker_index] ) {
605
605
  if ( skip_next ) {
606
606
  skip_next = false
607
607
  return null
@@ -609,55 +609,23 @@ app.get('/playlist', async function(req, res) {
609
609
 
610
610
  if (line.indexOf('#EXTINF:') == 0) {
611
611
  time_counter += parseFloat(line.substring(8, line.length-1))
612
+ session.debuglog('checking skip marker at ' + time_counter)
612
613
 
613
- if ( (inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) ) {
614
- let inning_index = 0
615
- if ( inning_number > 0 ) {
616
- inning_index = (inning_number * 2)
617
- if ( inning_half == 'top' ) inning_index = inning_index - 1
618
- }
619
- if ( (typeof session.temp_cache[contentId].inning_offsets[inning_index] !== 'undefined') && (typeof session.temp_cache[contentId].inning_offsets[inning_index].start !== 'undefined') && (time_counter < session.temp_cache[contentId].inning_offsets[inning_index].start) ) {
620
- session.debuglog('skipping ' + time_counter + ' before ' + session.temp_cache[contentId].inning_offsets[inning_index].start)
621
- // Increment skip index if our offset is less than the inning start
622
- if ( offsets && (offsets[skip_index]) && (offsets[skip_index].end) && (offsets[skip_index].end < session.temp_cache[contentId].inning_offsets[inning_index].start) ) {
623
- skip_index++
624
- }
625
- skip_next = true
626
- if ( discontinuity ) {
627
- return null
628
- } else {
629
- discontinuity = true
630
- return '#EXT-X-DISCONTINUITY'
631
- }
632
- } else {
633
- session.debuglog('inning start time not found or duplicate request made, ignoring: ' + u)
634
- inning_half = VALID_INNING_HALF[0]
635
- inning_number = VALID_INNING_NUMBER[0]
636
- }
614
+ while (skip_markers[skip_marker_index] && (skip_markers[skip_marker_index].break_end < time_counter)) {
615
+ skip_marker_index++
637
616
  }
638
-
639
- if ( (skip != VALID_SKIP[0]) && (inning_half == VALID_INNING_HALF[0]) && (inning_number == VALID_INNING_NUMBER[0]) ) {
640
- let skip_this = true
641
- if ( (typeof offsets[skip_index] !== 'undefined') && (typeof offsets[skip_index].start !== 'undefined') && (typeof offsets[skip_index].end !== 'undefined') && (time_counter > offsets[skip_index].start) && (time_counter > offsets[skip_index].end) ) {
642
- skip_index++
643
- }
644
- if ( (typeof offsets[skip_index] === 'undefined') || (typeof offsets[skip_index].start === 'undefined') || (typeof offsets[skip_index].end === 'undefined') || ((time_counter > offsets[skip_index].start) && (time_counter < offsets[skip_index].end)) ) {
645
- session.debuglog('keeping ' + time_counter)
646
- skip_this = false
647
- } else {
648
- session.debuglog('skipping ' + time_counter)
649
- }
650
- if ( skip_this ) {
651
- skip_next = true
652
- if ( discontinuity ) {
653
- return null
654
- } else {
655
- discontinuity = true
656
- return '#EXT-X-DISCONTINUITY'
657
- }
617
+ if (skip_markers[skip_marker_index] && (time_counter >= skip_markers[skip_marker_index].break_start) && (time_counter < skip_markers[skip_marker_index].break_end)) {
618
+ session.debuglog('skipping ' + time_counter)
619
+ skip_next = true
620
+ if ( discontinuity ) {
621
+ return null
658
622
  } else {
659
- discontinuity = false
623
+ discontinuity = true
624
+ return '#EXT-X-DISCONTINUITY'
660
625
  }
626
+ } else {
627
+ session.debuglog('keeping ' + time_counter)
628
+ discontinuity = false
661
629
  }
662
630
  }
663
631
  }
@@ -689,6 +657,19 @@ app.get('/playlist', async function(req, res) {
689
657
  })
690
658
  .join('\n')+'\n'
691
659
 
660
+ if ( pad != VALID_PAD[0] ) {
661
+ let body_array = body.trim().split('\n')
662
+ let last_segment_index = body_array.length-1
663
+ if ( body_array[last_segment_index] == '#EXT-X-ENDLIST' ) {
664
+ session.debuglog('padding archive stream with extra segments')
665
+ last_segment_index--
666
+ let pad_lines = '#EXT-X-DISCONTINUITY' + '\n' + body_array[last_segment_index-1] + '\n' + body_array[last_segment_index] + '\n'
667
+ session.debuglog(pad_lines)
668
+ for (i=0; i<pad; i++) {
669
+ body += pad_lines
670
+ }
671
+ }
672
+ }
692
673
  if ( force_vod != VALID_FORCE_VOD[0] ) body += '#EXT-X-ENDLIST' + '\n'
693
674
  session.debuglog(body)
694
675
  respond(response, res, Buffer.from(body))
@@ -795,6 +776,7 @@ app.get('/', async function(req, res) {
795
776
 
796
777
  let gameDate = session.liveDate()
797
778
  let todayUTCHours = session.getTodayUTCHours()
779
+ let curDate = new Date()
798
780
  if ( req.query.date ) {
799
781
  if ( req.query.date == VALID_DATES[1] ) {
800
782
  gameDate = session.yesterdayDate()
@@ -802,7 +784,6 @@ app.get('/', async function(req, res) {
802
784
  gameDate = req.query.date
803
785
  }
804
786
  } else {
805
- let curDate = new Date()
806
787
  let utcHours = curDate.getUTCHours()
807
788
  if ( (utcHours >= todayUTCHours) && (utcHours < YESTERDAY_UTC_HOURS) ) {
808
789
  gameDate = session.yesterdayDate()
@@ -823,6 +804,10 @@ app.get('/', async function(req, res) {
823
804
  if ( req.query.startFrom ) {
824
805
  startFrom = req.query.startFrom
825
806
  }
807
+ var controls = VALID_CONTROLS[0]
808
+ if ( req.query.controls ) {
809
+ controls = req.query.controls
810
+ }
826
811
  var scores = VALID_SCORES[0]
827
812
  if ( req.query.scores ) {
828
813
  scores = req.query.scores
@@ -855,9 +840,9 @@ app.get('/', async function(req, res) {
855
840
  if ( req.query.skip ) {
856
841
  skip = req.query.skip
857
842
  }
858
- var skip_adjust = 0
859
- if ( req.query.skip_adjust ) {
860
- skip_adjust = req.query.skip_adjust
843
+ var pad = VALID_PAD[0]
844
+ if ( req.query.pad ) {
845
+ pad = req.query.pad
861
846
  }
862
847
  // audio_url is disabled here, now used in multiview instead
863
848
  /*var audio_url = ''
@@ -890,13 +875,13 @@ app.get('/', async function(req, res) {
890
875
  body += '</style><script type="text/javascript">' + "\n";
891
876
 
892
877
  // Define option variables in page
893
- body += 'var date="' + gameDate + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";var force_vod="' + force_vod + '";var inning_half="' + inning_half + '";var inning_number="' + inning_number + '";var skip="' + skip + '";var skip_adjust="' + skip_adjust + '";var linkType="' + linkType + '";var startFrom="' + startFrom + '";var scores="' + scores + '";var scan_mode="' + scan_mode + '";' + "\n"
878
+ body += 'var date="' + gameDate + '";var mediaType="' + mediaType + '";var resolution="' + resolution + '";var audio_track="' + audio_track + '";var force_vod="' + force_vod + '";var inning_half="' + inning_half + '";var inning_number="' + inning_number + '";var skip="' + skip + '";var pad="' + pad + '";var linkType="' + linkType + '";var startFrom="' + startFrom + '";var scores="' + scores + '";var controls="' + controls + '";var scan_mode="' + scan_mode + '";' + "\n"
894
879
  // audio_url is disabled here, now used in multiview instead
895
880
  //body += 'var audio_url="' + audio_url + '";' + "\n"
896
881
 
897
882
  // Reload function, called after options change
898
883
  // audio_url is disabled here, now used in multiview instead
899
- body += 'var defaultDate="' + session.liveDate() + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + session.yesterdayDate() + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + session.liveDate() + '"){urldate="today"}else if (date == "' + session.yesterdayDate() + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}/*if (audio_url != ""){newurl+="audio_url="+encodeURIComponent(audio_url)+"&"}*/if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";if (skip_adjust != "0"){newurl+="skip_adjust="+skip_adjust+"&"}}}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="Embed"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
884
+ body += 'var defaultDate="' + session.liveDate() + '";var curDate=new Date();var utcHours=curDate.getUTCHours();if ((utcHours >= ' + todayUTCHours + ') && (utcHours < ' + YESTERDAY_UTC_HOURS + ')){defaultDate="' + session.yesterdayDate() + '"}function reload(){var newurl="/?";if (date != defaultDate){var urldate=date;if (date == "' + session.liveDate() + '"){urldate="today"}else if (date == "' + session.yesterdayDate() + '"){urldate="yesterday"}newurl+="date="+urldate+"&"}if (mediaType != "' + VALID_MEDIA_TYPES[0] + '"){newurl+="mediaType="+mediaType+"&"}if (mediaType=="Video"){if (resolution != "' + VALID_RESOLUTIONS[0] + '"){newurl+="resolution="+resolution+"&"}if (audio_track != "' + VALID_AUDIO_TRACKS[0] + '"){newurl+="audio_track="+encodeURIComponent(audio_track)+"&"}else if (resolution == "none"){newurl+="audio_track="+encodeURIComponent("' + VALID_AUDIO_TRACKS[2] + '")+"&"}/*if (audio_url != ""){newurl+="audio_url="+encodeURIComponent(audio_url)+"&"}*/if (inning_half != "' + VALID_INNING_HALF[0] + '"){newurl+="inning_half="+inning_half+"&"}if (inning_number != "' + VALID_INNING_NUMBER[0] + '"){newurl+="inning_number="+inning_number+"&"}if (skip != "' + VALID_SKIP[0] + '"){newurl+="skip="+skip+"&";}}if (pad != "' + VALID_PAD[0] + '"){newurl+="pad="+pad+"&";}if (linkType != "' + VALID_LINK_TYPES[0] + '"){newurl+="linkType="+linkType+"&"}if (linkType=="Embed"){if (startFrom != "' + VALID_START_FROM[0] + '"){newurl+="startFrom="+startFrom+"&"}if (controls != "' + VALID_CONTROLS[0] + '"){newurl+="controls="+controls+"&"}}if (linkType=="Stream"){if (force_vod != "' + VALID_FORCE_VOD[0] + '"){newurl+="force_vod="+force_vod+"&"}}if (scores != "' + VALID_SCORES[0] + '"){newurl+="scores="+scores+"&"}if (scan_mode != "' + session.data.scan_mode + '"){newurl+="scan_mode="+scan_mode+"&"}window.location=newurl.substring(0,newurl.length-1)}' + "\n"
900
885
 
901
886
  // Ajax function for multiview and highlights
902
887
  body += 'function makeGETRequest(url, callback){var request=new XMLHttpRequest();request.onreadystatechange=function(){if (request.readyState==4 && request.status==200){callback(request.responseText)}};request.open("GET", url);request.send();}' + "\n"
@@ -941,7 +926,15 @@ app.get('/', async function(req, res) {
941
926
 
942
927
  body += '<p>'
943
928
  if ( linkType == 'Embed' ) {
944
- body += '<span class="tooltip">Start From<span class="tooltiptext">For the embedded player only: Beginning will start playback at the beginning of the stream (may be 1 hour before game time for live games), and Live will start at the live point (if the event is live -- archive games should always start at the beginning). You can still seek anywhere.</span></span>: '
929
+ body += '<p><span class="tooltip">Video Controls<span class="tooltiptext">Choose whether to show or hide controls on the embedded video page. Helpful to avoid timeline spoilers.</span></span>: '
930
+ for (var i = 0; i < VALID_CONTROLS.length; i++) {
931
+ body += '<button '
932
+ if ( controls == VALID_CONTROLS[i] ) body += 'class="default" '
933
+ body += 'onclick="controls=\'' + VALID_CONTROLS[i] + '\';reload()">' + VALID_CONTROLS[i] + '</button> '
934
+ }
935
+ body += '</p>' + "\n"
936
+
937
+ body += '<p><span class="tooltip">Start From<span class="tooltiptext">For the embedded player only: Beginning will start playback at the beginning of the stream (may be 1 hour before game time for live games), and Live will start at the live point (if the event is live -- archive games should always start at the beginning). You can still seek anywhere.</span></span>: '
945
938
  for (var i = 0; i < VALID_START_FROM.length; i++) {
946
939
  body += '<button '
947
940
  if ( startFrom == VALID_START_FROM[i] ) body += 'class="default" '
@@ -1017,7 +1010,14 @@ app.get('/', async function(req, res) {
1017
1010
  if ( (currentDate >= compareStart) && (currentDate < compareEnd) ) {
1018
1011
  let querystring = '?event=biginning'
1019
1012
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION
1013
+ if ( linkType == 'embed' ) {
1014
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1015
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1016
+ }
1020
1017
  if ( resolution != VALID_RESOLUTIONS[0] ) querystring += '&resolution=' + resolution
1018
+ if ( linkType == 'stream' ) {
1019
+ if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
1020
+ }
1021
1021
  querystring += content_protect_b
1022
1022
  multiviewquerystring += content_protect_b
1023
1023
  body += '<a href="' + thislink + querystring + '">Big Inning</a>'
@@ -1229,7 +1229,11 @@ app.get('/', async function(req, res) {
1229
1229
  station += '*'
1230
1230
  }
1231
1231
  if ( (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON') || (cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ARCHIVE') || cache_data.dates[0].games[j].gameUtils.isFinal ) {
1232
- game_started = true
1232
+ let gameTime = new Date(cache_data.dates[0].games[j].gameDate)
1233
+ gameTime.setMinutes(gameTime.getMinutes()-10)
1234
+ if ( curDate >= gameTime ) {
1235
+ game_started = true
1236
+ }
1233
1237
  let mediaId = cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaId
1234
1238
  if ( (mediaType == 'MLBTV') && session.cache.media && session.cache.media[mediaId] && session.cache.media[mediaId].blackout && session.cache.media[mediaId].blackoutExpiry && (new Date(session.cache.media[mediaId].blackoutExpiry) > new Date()) ) {
1235
1239
  body += teamabbr + ': <s>' + station + '</s>'
@@ -1238,13 +1242,13 @@ app.get('/', async function(req, res) {
1238
1242
  querystring = '?mediaId=' + mediaId
1239
1243
  let multiviewquerystring = querystring + '&resolution=' + DEFAULT_MULTIVIEW_RESOLUTION + '&audio_track=' + DEFAULT_MULTIVIEW_AUDIO_TRACK
1240
1244
  if ( linkType == 'embed' ) {
1241
- if ( startFrom != 'Beginning' ) querystring += '&startFrom=' + startFrom
1245
+ if ( startFrom != VALID_START_FROM[0] ) querystring += '&startFrom=' + startFrom
1246
+ if ( controls != VALID_CONTROLS[0] ) querystring += '&controls=' + controls
1242
1247
  }
1243
1248
  if ( mediaType == 'MLBTV' ) {
1244
1249
  if ( inning_half != VALID_INNING_HALF[0] ) querystring += '&inning_half=' + inning_half
1245
- if ( inning_number != '' ) querystring += '&inning_number=' + relative_inning
1246
- if ( skip != 'off' ) querystring += '&skip=' + skip
1247
- if ( skip_adjust != '0' ) querystring += '&skip_adjust=' + skip_adjust
1250
+ if ( inning_number != VALID_INNING_NUMBER[0] ) querystring += '&inning_number=' + relative_inning
1251
+ if ( skip != VALID_SKIP[0] ) querystring += '&skip=' + skip
1248
1252
  if ( (inning_half != VALID_INNING_HALF[0]) || (inning_number != VALID_INNING_NUMBER[0]) || (skip != VALID_SKIP[0]) ) {
1249
1253
  let contentId = cache_data.dates[0].games[j].content.media.epg[k].items[x].contentId
1250
1254
  querystring += '&contentId=' + contentId
@@ -1254,6 +1258,7 @@ app.get('/', async function(req, res) {
1254
1258
  // audio_url is disabled here, now used in multiview instead
1255
1259
  //if ( audio_url != '' ) querystring += '&audio_url=' + encodeURIComponent(audio_url)
1256
1260
  }
1261
+ if ( pad != VALID_PAD[0] ) querystring += '&pad=' + pad
1257
1262
  if ( linkType == 'stream' ) {
1258
1263
  if ( cache_data.dates[0].games[j].content.media.epg[k].items[x].mediaState == 'MEDIA_ON' ) {
1259
1264
  if ( force_vod != VALID_FORCE_VOD[0] ) querystring += '&force_vod=' + force_vod
@@ -1295,7 +1300,7 @@ app.get('/', async function(req, res) {
1295
1300
  if ( body.substr(-2) == ', ' ) {
1296
1301
  body = body.slice(0, -2)
1297
1302
  }
1298
- if ( (mediaType == 'MLBTV') && (game_started) ) {
1303
+ if ( (mediaType == 'MLBTV') && (game_started) && cache_data.dates[0].games[j].content && cache_data.dates[0].games[j].content.summary && cache_data.dates[0].games[j].content.summary.hasHighlightsVideo ) {
1299
1304
  body += '<br/><a href="javascript:showhighlights(\'' + cache_data.dates[0].games[j].gamePk + '\',\'' + gameDate + '\')">Highlights</a>'
1300
1305
  }
1301
1306
  if ( body.substr(-2) == ', ' ) {
@@ -1336,15 +1341,24 @@ app.get('/', async function(req, res) {
1336
1341
  //body += '<br/><span class="tooltip">or enter a separate audio stream URL<span class="tooltiptext">EXPERIMENTAL! May not actually work. For video streams only: you can also include a separate audio stream URL as an alternate audio track. This is useful if you want to pair the road radio feed with a national TV broadcast (which only includes home radio feeds by default).<br/><br/>After entering the audio stream URL, click the Update button to include it in the video links above; click the Reset button when done with this option.<br/><br/>Warning: does not support inning start or skip options.</span></span>: <span class="tinytext">(copy one from the <button onclick="mediaType=\'Audio\';reload()">Audio</button> page</a>)</span><br/><textarea id="audio_url" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)">' + audio_url + '</textarea><br/><button onclick="audio_url=document.getElementById(\'audio_url\').value;reload()">Update Audio URL</button> <button onclick="audio_url=\'\';reload()">Reset Audio URL</button><br/>'
1337
1342
  body += '</p>' + "\n"
1338
1343
 
1339
- body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove breaks or non-decision pitches from the stream (the latter is useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream.</span></span>: '
1344
+ body += '<p><span class="tooltip">Skip<span class="tooltiptext">For video streams only (use the video "none" option above to apply it to audio streams): you can remove breaks, idle time, or non-action pitches from the stream (useful to make your own "condensed games").<br/><br/>NOTE: skip timings are only generated when the stream is loaded -- so for live games, it will only skip up to the time you loaded the stream.</span></span>: '
1340
1345
  for (var i = 0; i < VALID_SKIP.length; i++) {
1341
1346
  body += '<button '
1342
1347
  if ( skip == VALID_SKIP[i] ) body += 'class="default" '
1343
1348
  body += 'onclick="skip=\'' + VALID_SKIP[i] + '\';reload()">' + VALID_SKIP[i] + '</button> '
1344
1349
  }
1345
- body += ' <span class="tooltip">Skip Adjust<span class="tooltiptext">Seconds to adjust the skip time video segments, if necessary. Try a negative number if the plays are ending before the video segments begin; use a positive number if the video segments are ending before the play happens.</span></span>: <input type="number" id="skip_adjust" value="' + skip_adjust + '" step="5" onchange="setTimeout(function(){skip_adjust=document.getElementById(\'skip_adjust\').value;reload()},750)" onblur="skip_adjust=this.value;reload()" style="vertical-align:top;font-size:.8em;width:3em"/>'
1346
1350
  body += '</p>' + "\n"
1351
+ }
1352
+
1353
+ body += '<p><span class="tooltip">Pad<span class="tooltiptext">You can pad archive streams with random extra time at the end, to help conceal timeline spoilers.</span></span>: '
1354
+ for (var i = 0; i < VALID_PAD.length; i++) {
1355
+ body += '<button '
1356
+ if ( pad == VALID_PAD[i] ) body += 'class="default" '
1357
+ body += 'onclick="pad=\'' + VALID_PAD[i] + '\';reload()">' + VALID_PAD[i] + '</button> '
1358
+ }
1359
+ body += '</p>' + "\n"
1347
1360
 
1361
+ if ( mediaType == 'Video' ) {
1348
1362
  body += '<table><tr><td><table><tr><td>1</td><td>2</tr><tr><td>3</td><td>4</td></tr></table><td><span class="tooltip">Multiview / Alternate Audio / Sync<span class="tooltiptext">For video streams only: create a new live stream combining 1-4 separate video streams, using the layout shown at left (if more than 1 video stream is selected). Check the boxes next to feeds above to add/remove them, then click "Start" when ready, "Stop" when done watching, or "Restart" to stop and start with the currently selected streams. May take up to 15 seconds after starting before it is ready to play.<br/><br/>No video scaling is performed: defaults to 540p video for each stream, which can combine to make one 1080p stream. Audio defaults to English (TV) audio. If you specify a different audio track instead, you can use the box after each URL below to adjust the sync in seconds (use positive values if audio is early and the audio stream needs to be padded with silence at the beginning to line up with the video; negative values if audio is late, and audio needs to be trimmed from the beginning.)<br/><br/>TIP #1: You can enter just 1 video stream here, at any resolution, to take advantage of the audio sync or alternate audio features without using multiview -- a single video stream will not be re-encoded and will be presented at its full resolution.<br/><br/>TIP #2: You can also manually enter streams from other sources like <a href="https://www.npmjs.com/package/milbserver" target="_blank">milbserver</a> in the boxes below.<br/><br/>WARNING #1: if the mlbserver process dies or restarts while multiview is active, the ffmpeg encoding process will be orphaned and must be killed manually.<br/><br/>WARNING #2: If you did not specify a hardware encoder for ffmpeg on the command line, this will use your server CPU for encoding. Either way, your system may not be able to keep up with processing 4 video streams at once. Try fewer streams if you have perisistent trouble.</span></span>: <a id="startmultiview" href="" onclick="startmultiview(this);return false">Start'
1349
1363
  if ( ffmpeg_status ) body += 'ed'
1350
1364
  body += '</a> | <a id="stopmultiview" href="" onclick="stopmultiview(this);return false">Stop'
@@ -1394,7 +1408,7 @@ app.get('/', async function(req, res) {
1394
1408
 
1395
1409
  body += '<p><span class="tooltip">Include (or exclude) LIDOM<span class="tooltiptext">Dominican Winter League, aka Liga de Beisbol Dominicano. Live stream only, does not support starting from the beginning or certain innings, skip options, etc.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=lidom' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=lidom' + content_protect_b + '">guide.xml</a></p>' + "\n"
1396
1410
 
1397
- body += '<p><span class="tooltip">Include (or exclude) Big Inning<span class="tooltiptext">Big Inning is the live look-in and highlights show. Live stream only, does not support starting from the beginning.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">guide.xml</a></p>' + "\n"
1411
+ body += '<p><span class="tooltip">Include (or exclude) Big Inning<span class="tooltiptext">Big Inning is the live look-in and highlights show.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=biginning' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=biginning' + content_protect_b + '">guide.xml</a></p>' + "\n"
1398
1412
 
1399
1413
  body += '<p><span class="tooltip">Include (or exclude) Multiview<span class="tooltiptext">Requires starting and stopping the multiview stream from the web interface.</span></span>: <a href="/channels.m3u?mediaType=' + mediaType + '&resolution=' + resolution + '&includeTeams=multiview' + content_protect_b + '">channels.m3u</a> and <a href="/guide.xml?mediaType=' + mediaType + '&includeTeams=multiview' + content_protect_b + '">guide.xml</a></p>' + "\n"
1400
1414
 
@@ -1529,10 +1543,14 @@ app.get('/embed.html', async function(req, res) {
1529
1543
 
1530
1544
  delete req.headers.host
1531
1545
 
1532
- let startFrom = 'Beginning'
1546
+ let startFrom = VALID_START_FROM[0]
1533
1547
  if ( req.query.startFrom ) {
1534
1548
  startFrom = req.query.startFrom
1535
1549
  }
1550
+ let controls = VALID_CONTROLS[0]
1551
+ if ( req.query.controls ) {
1552
+ controls = req.query.controls
1553
+ }
1536
1554
 
1537
1555
  let video_url = '/stream.m3u8'
1538
1556
  if ( req.query.src ) {
@@ -1546,9 +1564,13 @@ app.get('/embed.html', async function(req, res) {
1546
1564
  session.debuglog('embed src : ' + video_url)
1547
1565
 
1548
1566
  // Adapted from https://hls-js.netlify.app/demo/basic-usage.html
1549
- var body = '<html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><title>' + appname + ' player</title><link rel="icon" href="favicon.svg"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{background-color:black;color:lightgrey;font-family:Arial,Helvetica,sans-serif}video{width:100% !important;height:auto !important;max-width:1280px}input[type=number]::-webkit-inner-spin-button{opacity:1}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}</style><script>function goBack(){var prevPage=window.location.href;window.history.go(-1);setTimeout(function(){if(window.location.href==prevPage){window.location.href="/"}}, 500)}function toggleAudio(x){var elements=document.getElementsByClassName("audioButton");for(var i=0;i<elements.length;i++){elements[i].className="audioButton"}document.getElementById("audioButton"+x).className+=" default";hls.audioTrack=x}function changeTime(x){video.currentTime+=x}function changeRate(x){let newRate=Math.round((Number(document.getElementById("playback_rate").value)+x)*10)/10;if((newRate<=document.getElementById("playback_rate").max) && (newRate>=document.getElementById("playback_rate").min)){document.getElementById("playback_rate").value=newRate.toFixed(1);video.defaultPlaybackRate=video.playbackRate=document.getElementById("playback_rate").value}}function myKeyPress(e){if(e.key=="ArrowRight"){changeTime(10)}else if(e.key=="ArrowLeft"){changeTime(-10)}else if(e.key=="ArrowUp"){changeRate(0.1)}else if(e.key=="ArrowDown"){changeRate(-0.1)}}</script></head><body onkeydown="myKeyPress(event)"><script src="https://hls-js.netlify.app/dist/hls.js"></script><video id="video" controls></video><script>var video=document.getElementById("video");if(Hls.isSupported()){var hls=new Hls('
1567
+ var body = '<html><head><meta charset="UTF-8"><meta http-equiv="Content-type" content="text/html;charset=UTF-8"><title>' + appname + ' player</title><link rel="icon" href="favicon.svg"><style type="text/css">input[type=text],input[type=button]{-webkit-appearance:none;-webkit-border-radius:0}body{background-color:black;color:lightgrey;font-family:Arial,Helvetica,sans-serif}video{width:100% !important;height:auto !important;max-width:1280px}input[type=number]::-webkit-inner-spin-button{opacity:1}button{color:lightgray;background-color:black}button.default{color:black;background-color:lightgray}</style><script>function goBack(){var prevPage=window.location.href;window.history.go(-1);setTimeout(function(){if(window.location.href==prevPage){window.location.href="/"}}, 500)}function toggleAudio(x){var elements=document.getElementsByClassName("audioButton");for(var i=0;i<elements.length;i++){elements[i].className="audioButton"}document.getElementById("audioButton"+x).className+=" default";hls.audioTrack=x}function changeTime(x){video.currentTime+=x}function changeRate(x){let newRate=Math.round((Number(document.getElementById("playback_rate").value)+x)*10)/10;if((newRate<=document.getElementById("playback_rate").max) && (newRate>=document.getElementById("playback_rate").min)){document.getElementById("playback_rate").value=newRate.toFixed(1);video.defaultPlaybackRate=video.playbackRate=document.getElementById("playback_rate").value}}function myKeyPress(e){if(e.key=="ArrowRight"){changeTime(10)}else if(e.key=="ArrowLeft"){changeTime(-10)}else if(e.key=="ArrowUp"){changeRate(0.1)}else if(e.key=="ArrowDown"){changeRate(-0.1)}}</script></head><body onkeydown="myKeyPress(event)"><script src="https://hls-js.netlify.app/dist/hls.js"></script><video id="video"'
1568
+ if ( controls == VALID_CONTROLS[0] ) {
1569
+ body += ' controls'
1570
+ }
1571
+ body += '></video><script>var video=document.getElementById("video");if(Hls.isSupported()){var hls=new Hls('
1550
1572
 
1551
- if ( startFrom != 'Live' ) {
1573
+ if ( startFrom != VALID_START_FROM[1] ) {
1552
1574
  body += '{startPosition:0,liveSyncDuration:32400,liveMaxLatencyDuration:32410}'
1553
1575
  }
1554
1576
 
@@ -1556,7 +1578,7 @@ app.get('/embed.html', async function(req, res) {
1556
1578
 
1557
1579
  body += '<button onclick="changeTime(video.duration-10)">Latest</button> '
1558
1580
 
1559
- body += '<button id="airplay">AirPlay</button></p><p>Playback rate: <input type="number" value=1.0 min=0.1 max=16.0 step=0.1 id="playback_rate" size="8" style="width: 4em" onchange="video.defaultPlaybackRate=video.playbackRate=this.value"></p><p>Audio: <button onclick="video.muted=!video.muted">Toggle Mute</button> <span id="audioSpan"></span></p><p><button onclick="goBack()">Back</button></p><script>var airPlay=document.getElementById("airplay");if(window.WebKitPlaybackTargetAvailabilityEvent){video.addEventListener("webkitplaybacktargetavailabilitychanged",function(event){switch(event.availability){case "available":airPlay.style.display="inline";break;default:airPlay.style.display="none"}airPlay.addEventListener("click",function(){video.webkitShowPlaybackTargetPicker()})})}else{airPlay.style.display="none"}</script></body></html>'
1581
+ body += '<button id="airplay">AirPlay</button></p><p>Playback rate: <input type="number" value=1.0 min=0.1 max=16.0 step=0.1 id="playback_rate" size="8" style="width: 4em" onchange="video.defaultPlaybackRate=video.playbackRate=this.value"></p><p>Audio: <button onclick="video.muted=!video.muted">Toggle Mute</button> <span id="audioSpan"></span></p><p>Controls: <button onclick="video.controls=!video.controls">Toggle Controls</button></p><p><button onclick="goBack()">Back</button></p><script>var airPlay=document.getElementById("airplay");if(window.WebKitPlaybackTargetAvailabilityEvent){video.addEventListener("webkitplaybacktargetavailabilitychanged",function(event){switch(event.availability){case "available":airPlay.style.display="inline";break;default:airPlay.style.display="none"}airPlay.addEventListener("click",function(){video.webkitShowPlaybackTargetPicker()})})}else{airPlay.style.display="none"}</script></body></html>'
1560
1582
  res.end(body)
1561
1583
  })
1562
1584
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlbserver",
3
- "version": "2022.03.27",
3
+ "version": "2022.05.09",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/session.js CHANGED
@@ -33,6 +33,14 @@ const TODAY_UTC_HOURS = 8 // UTC hours (EST + 4) into tomorrow to still use toda
33
33
 
34
34
  const TEAM_IDS = {'ARI':'109','ATL':'144','BAL':'110','BOS':'111','CHC':'112','CWS':'145','CIN':'113','CLE':'114','COL':'115','DET':'116','HOU':'117','KC':'118','LAA':'108','LAD':'119','MIA':'146','MIL':'158','MIN':'142','NYM':'121','NYY':'147','OAK':'133','PHI':'143','PIT':'134','STL':'138','SD':'135','SF':'137','SEA':'136','TB':'139','TEX':'140','TOR':'141','WSH':'120'}
35
35
 
36
+ // These are the events to ignore, if we're skipping breaks
37
+ const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
38
+ // These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
39
+ const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff']
40
+ const EVENT_START_PADDING = -5
41
+ const EVENT_END_PADDING = 8
42
+ const MINIMUM_BREAK_DURATION = 10
43
+
36
44
  class sessionClass {
37
45
  // Initialize the class
38
46
  constructor(argv = {}) {
@@ -1571,14 +1579,15 @@ class sessionClass {
1571
1579
  if ( (excludeTeams.length > 0) && (excludeTeams.includes(team) || excludeTeams.includes(opponent_team) || excludeTeams.includes(teamType)) ) {
1572
1580
  continue
1573
1581
  } else if ( (includeTeams.length == 0) || includeTeams.includes(team) || includeTeams.includes(teamType) ) {
1574
- let icon = server
1582
+ /*let icon = server
1575
1583
  if ( (teamType == 'NATIONAL') && ((includeTeams.length == 0) || ((includeTeams.length > 0) && includeTeams.includes(teamType))) ) {
1576
1584
  team = teamType + '.' + nationalCounter[mediaTitle]
1577
1585
  icon += '/image.svg?teamId=MLB'
1578
1586
  } else {
1579
1587
  icon += '/image.svg?teamId=' + cache_data.dates[i].games[j].content.media.epg[k].items[x].mediaFeedSubType
1580
1588
  }
1581
- if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
1589
+ if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect*/
1590
+ let icon = 'https://img.mlbstatic.com/mlb-photos/image/upload/ar_167:215,c_crop/fl_relative,l_team:' + cache_data.dates[i].games[j].teams['home'].team.id + ':fill:spot.png,w_1.0,h_1,x_0.5,y_0,fl_no_overflow,e_distort:100p:0:200p:0:200p:100p:0:100p/fl_relative,l_team:' + cache_data.dates[i].games[j].teams['away'].team.id + ':logo:spot:current,w_0.38,x_-0.25,y_-0.16/fl_relative,l_team:' + cache_data.dates[i].games[j].teams['home'].team.id + ':logo:spot:current,w_0.38,x_0.25,y_0.16/w_750/team/' + cache_data.dates[i].games[j].teams['away'].team.id + '/fill/spot.png'
1582
1591
  let channelid = mediaType + '.' + team
1583
1592
  channels[channelid] = {}
1584
1593
  channels[channelid].name = channelid
@@ -1661,8 +1670,9 @@ class sessionClass {
1661
1670
  if ( (excludeTeams.length > 0) && excludeTeams.includes('BIGINNING') ) {
1662
1671
  // do nothing
1663
1672
  } else if ( (includeTeams.length == 0) || includeTeams.includes('BIGINNING') ) {
1664
- let icon = server + '/image.svg?teamId=MLB'
1665
- if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect
1673
+ /*let icon = server + '/image.svg?teamId=MLB'
1674
+ if ( this.protection.content_protect ) icon += '&content_protect=' + this.protection.content_protect*/
1675
+ let icon = 'https://img.mlbstatic.com/mlb-images/image/private/ar_16:9,g_auto,q_auto:good,w_372,c_fill,f_jpg/mlb/uwr8vepua4t1fe8uwyki'
1666
1676
  let channelid = mediaType + '.BIGINNING'
1667
1677
  channels[channelid] = {}
1668
1678
  channels[channelid].name = channelid
@@ -1998,164 +2008,140 @@ class sessionClass {
1998
2008
  }
1999
2009
  }
2000
2010
 
2001
- // Get event offsets into temporary cache
2002
- async getEventOffsets(contentId, skip_types, skip_adjust = 0) {
2011
+ // Get skip markers into temporary cache
2012
+ async getSkipMarkers(contentId, skip_type, start_inning, start_inning_half) {
2003
2013
  try {
2004
- this.debuglog('getEventOffsets')
2014
+ this.debuglog('getSkipMarkers')
2015
+
2016
+ let skip_markers = []
2005
2017
 
2006
- if ( skip_adjust != 0 ) session.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
2018
+ // assume the game starts in a break
2019
+ let break_start = 0
2007
2020
 
2008
2021
  // Get the broadcast start time first -- event times will be relative to this
2009
2022
  let broadcast_start = await this.getBroadcastStart(contentId)
2010
2023
  let broadcast_start_offset = broadcast_start.broadcast_start_offset
2011
2024
  let broadcast_start_timestamp = broadcast_start.broadcast_start_timestamp
2012
- this.debuglog('broadcast start detected as ' + broadcast_start_timestamp + ', offset ' + broadcast_start_offset)
2013
-
2014
- let cache_data = await this.getGamedayData(contentId)
2025
+ this.debuglog('getSkipMarkers broadcast start detected as ' + broadcast_start_timestamp + ', offset ' + broadcast_start_offset)
2015
2026
 
2016
- // There are the events to ignore, if we're skipping breaks
2017
- let break_types = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
2027
+ // start inning 0 is simply the broadcast start offset
2028
+ if ((skip_type == 0) && (start_inning == 0)) {
2029
+ let break_end = broadcast_start_offset
2030
+ skip_markers.push({'break_start': break_start, 'break_end': break_end})
2031
+ } else {
2032
+ if (start_inning == '') {
2033
+ start_inning = 0
2034
+ }
2035
+ if (start_inning_half == '') {
2036
+ start_inning_half = 'top'
2037
+ }
2018
2038
 
2019
- // There are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
2020
- let action_types = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Out', 'Balk', 'Defensive Indiff']
2039
+ let cache_data = await this.getGamedayData(contentId)
2021
2040
 
2022
- let inning_offsets = [{start:broadcast_start_offset}]
2023
- let event_offsets = [{start:0}]
2024
- let last_event = 0
2025
- let default_event_duration = 15
2041
+ // make sure we have play data
2042
+ if (cache_data && cache_data.liveData && cache_data.liveData.plays && cache_data.liveData.plays.allPlays) {
2026
2043
 
2027
- // Pad times by these amounts
2028
- let pad_start = 0
2029
- let pad_end = 15
2030
- let pad_adjust = 20
2044
+ // keep track of inning, if skipping inning breaks only
2045
+ let previous_inning = 0
2046
+ let previous_inning_half = ''
2031
2047
 
2032
- // Inning counters
2033
- let last_inning = 0
2034
- let last_inning_half = ''
2048
+ // calculate total skip time (for fun)
2049
+ let total_skip_time = 0
2035
2050
 
2036
- // Loop through all plays
2037
- for (var i=0; i < cache_data.liveData.plays.allPlays.length; i++) {
2051
+ // Loop through all plays
2052
+ for (var i=0; i < cache_data.liveData.plays.allPlays.length; i++) {
2038
2053
 
2039
- // If requested, calculate inning offsets
2040
- if ( skip_types.includes('innings') ) {
2041
- // Look for a change from our inning counters
2042
- if ( cache_data.liveData.plays.allPlays[i].about && cache_data.liveData.plays.allPlays[i].about.inning && ((cache_data.liveData.plays.allPlays[i].about.inning != last_inning) || (cache_data.liveData.plays.allPlays[i].about.halfInning != last_inning_half)) ) {
2043
- let inning_index = cache_data.liveData.plays.allPlays[i].about.inning * 2
2044
- // top
2045
- if ( cache_data.liveData.plays.allPlays[i].about.halfInning == 'top' ) {
2046
- inning_index = inning_index - 1
2047
- }
2048
- if ( typeof inning_offsets[inning_index] === 'undefined' ) inning_offsets.push({})
2049
- for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
2050
- if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (break_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
2051
- // ignore break events
2052
- } else {
2053
- inning_offsets[inning_index].start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[j].startTime) - broadcast_start_timestamp) / 1000) - pad_start + skip_adjust
2054
- break
2054
+ // make sure start inning is valid
2055
+ if (start_inning > 0) {
2056
+ let last_play_index = cache_data.liveData.plays.allPlays.length - 1
2057
+ let final_inning = cache_data.liveData.plays.allPlays[last_play_index].about.inning
2058
+ if (start_inning >= final_inning) {
2059
+ if (start_inning > final_inning) {
2060
+ start_inning = final_inning
2061
+ let final_inning_half = json_source['liveData']['plays']['allPlays'][last_play_index]['about']['halfInning']
2062
+ if ((start_inning_half == 'bottom') && (final_inning_half == 'top')) {
2063
+ start_inning_half = final_inning_half
2064
+ }
2065
+ }
2055
2066
  }
2056
2067
  }
2057
- // Update inning counters
2058
- last_inning = cache_data.liveData.plays.allPlays[i].about.inning
2059
- last_inning_half = cache_data.liveData.plays.allPlays[i].about.halfInning
2060
- }
2061
- }
2062
2068
 
2063
- // Get event offsets, if necessary
2064
- if ( skip_types.includes('breaks') || skip_types.includes('pitches') ) {
2065
-
2066
- // Loop through play events, looking for actions
2067
- let actions = []
2068
- for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
2069
- // If skipping breaks, everything is an action except break types
2070
- // otherwise, only action types are included (skipping pitches)
2071
- if ( skip_types.includes('breaks') ) {
2072
- if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (break_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
2073
- // ignore break events
2074
- } else {
2075
- actions.push(j)
2076
- }
2077
- } else if ( cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && (action_types.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
2078
- actions.push(j)
2069
+ // exit loop after found inning, if not skipping any breaks
2070
+ if ((skip_type == 0) && (skip_markers.length == 1)) {
2071
+ break
2079
2072
  }
2080
- }
2081
2073
 
2082
- // Process breaks
2083
- if ( skip_types.includes('breaks') ) {
2084
- let this_event = {}
2085
- let event_in_atbat = false
2086
- for (var x=0; x < actions.length; x++) {
2087
- let this_pad_start = 0
2088
- let this_pad_end = 0
2089
- // Once we define each event's start time, we won't change it
2090
- if ( typeof this_event.start === 'undefined' ) {
2091
- this_event.start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) - pad_start + skip_adjust
2092
- // For events within at-bats, adjust the padding
2093
- if ( event_in_atbat ) {
2094
- this_event.start -= pad_adjust
2074
+ let current_inning = cache_data.liveData.plays.allPlays[i].about.inning
2075
+ let current_inning_half = cache_data.liveData.plays.allPlays[i].about.halfInning
2076
+ // make sure we're past our start inning
2077
+ if ((current_inning > start_inning) || ((current_inning == start_inning) && ((current_inning_half == start_inning_half) || (current_inning_half == 'bottom')))) {
2078
+ // loop through events within each play
2079
+ for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
2080
+ // always exclude break types
2081
+ if (cache_data.liveData.plays.allPlays[i].playEvents[j].details && cache_data.liveData.plays.allPlays[i].playEvents[j].details.event && BREAK_TYPES.includes(cache_data.liveData.plays.allPlays[i].playEvents[j].details.event)) {
2082
+ // if we're in the process of skipping inning breaks, treat the first break type we find as another inning break
2083
+ if ((skip_type == 1) && (previous_inning > 0)) {
2084
+ break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[j].startTime) - broadcast_start_timestamp) / 1000) + EVENT_END_PADDING
2085
+ previous_inning = 0
2086
+ }
2087
+ continue
2088
+ } else {
2089
+ let action_index
2090
+ // skip type 1 (breaks) && 2 (idle time) will look at all plays with an endTime
2091
+ if ((skip_type <= 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
2092
+ action_index = j
2093
+ } else if (skip_type == 3) {
2094
+ // skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
2095
+ if ( (j < (cache_data.liveData.plays.allPlays[i].playEvents.length - 1)) && (!cache_data.liveData.plays.allPlays[i].playEvents[j].details || !cache_data.liveData.plays.allPlays[i].playEvents[j].details.event || !ACTION_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.event.includes(v))) ) {
2096
+ continue
2097
+ } else {
2098
+ // if the action is associated with another play or the event doesn't have an end time, use the previous event instead
2099
+ if (cache_data.liveData.plays.allPlays[i].playEvents[j].actionPlayId || ((cache_data.liveData.plays.allPlays[i].playEvents[j].endTime === 'undefined') && (j > 0))) {
2100
+ action_index = j - 1
2101
+ } else {
2102
+ action_index = j
2103
+ }
2104
+ }
2105
+ }
2106
+ if (typeof action_index === 'undefined') {
2107
+ continue
2108
+ } else {
2109
+ let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + EVENT_START_PADDING
2110
+ // if the break duration should be greater than than our specified minimum
2111
+ // and if skip type is not 1 (inning breaks) or the inning has changed
2112
+ // then we'll add the skip marker
2113
+ // otherwise we'll ignore it and move on to the next one
2114
+ if ( ((break_end - break_start) >= MINIMUM_BREAK_DURATION) && ((skip_type != 1) || (current_inning != previous_inning) || (current_inning_half != previous_inning_half)) ) {
2115
+ skip_markers.push({'break_start': break_start, 'break_end': break_end})
2116
+ total_skip_time += break_end - break_start
2117
+ previous_inning = current_inning
2118
+ previous_inning_half = current_inning_half
2119
+ // exit loop after found inning, if not skipping breaks
2120
+ if (skip_type == 0) {
2121
+ break
2122
+ }
2123
+ }
2124
+ break_start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].endTime) - broadcast_start_timestamp) / 1000) + EVENT_END_PADDING
2125
+ // add extra padding for overturned review plays
2126
+ if (cache_data.liveData.plays.allPlays[i].reviewDetails && (cache_data.liveData.plays.allPlays[i].reviewDetails.isOverturned == true)) {
2127
+ break_start += 40
2128
+ }
2129
+ }
2095
2130
  }
2096
2131
  }
2097
- // Update the end time, if available
2098
- if ( cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime ) {
2099
- this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime) - broadcast_start_timestamp) / 1000) + pad_end + skip_adjust
2100
- // Otherwise use the start time to estimate the end time
2101
- } else {
2102
- this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust + default_event_duration
2103
- }
2104
- // Check if we have skipped a play event (indicating a break inside an at-bat), in which case push this event and start another one
2105
- if ( (x > 0) && (actions[x] > (actions[x-1]+1)) && (typeof this_event.end !== 'undefined') ) {
2106
- // For events within at-bats, adjust the padding
2107
- event_in_atbat = true
2108
- this_event.end += pad_adjust
2109
- event_offsets.push(this_event)
2110
- this_event = {}
2111
- }
2112
- }
2113
- // Once we've finished our loop through a play's events, push the event as long as we got an end time
2114
- if ( typeof this_event.end !== 'undefined' ) {
2115
- event_offsets.push(this_event)
2116
- }
2117
- } else if ( skip_types.includes('pitches') ) {
2118
- // If we're skipping pitches, but we didn't detect any action events, use the last play event
2119
- if ( (cache_data.liveData.plays.allPlays[i].playEvents.length > 0) && ((actions.length == 0) || (actions[(actions.length-1)] < (cache_data.liveData.plays.allPlays[i].playEvents.length-1))) ) {
2120
- actions.push(cache_data.liveData.plays.allPlays[i].playEvents.length-1)
2121
- }
2122
- // Loop through the actions
2123
- for (var x=0; x < actions.length; x++) {
2124
- let this_event = {}
2125
- let this_pad_start = pad_start
2126
- let this_pad_end = pad_end
2127
- // For events within at-bats, adjust the padding
2128
- if ( x < (actions.length-1) ) {
2129
- this_pad_start += pad_adjust
2130
- this_pad_end -= pad_adjust
2131
- }
2132
- this_event.start = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) - this_pad_start + skip_adjust
2133
- // If play event end time is available, set it and push this event
2134
- if ( cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime ) {
2135
- this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].endTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust
2136
- // Otherwise use the start time to estimate the end time
2137
- } else {
2138
- this_event.end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[actions[x]].startTime) - broadcast_start_timestamp) / 1000) + this_pad_end + skip_adjust + default_event_duration
2139
- }
2140
- event_offsets.push(this_event)
2141
2132
  }
2142
2133
  }
2143
- }
2144
- }
2145
2134
 
2146
- if ( skip_types.includes('innings') ) {
2147
- this.debuglog('inning offsets: ' + JSON.stringify(inning_offsets))
2135
+ this.debuglog('getSkipMarkers found ' + new Date(total_skip_time * 1000).toISOString().substr(11, 8) + ' total skip time')
2136
+ }
2148
2137
  }
2149
- this.temp_cache[contentId].inning_offsets = inning_offsets
2150
2138
 
2151
- if ( skip_types.includes('breaks') || skip_types.includes('pitches') ) {
2152
- this.debuglog('event offsets: ' + JSON.stringify(event_offsets))
2153
- }
2154
- this.temp_cache[contentId].event_offsets = event_offsets
2139
+ this.debuglog('getSkipMarkers skip markers: ' + JSON.stringify(skip_markers))
2140
+ this.temp_cache[contentId].skip_markers = skip_markers
2155
2141
 
2156
2142
  return true
2157
2143
  } catch(e) {
2158
- this.log('getEventOffsets error : ' + e.message)
2144
+ this.log('getSkipMarkers error : ' + e.message)
2159
2145
  }
2160
2146
  }
2161
2147
 
@@ -2347,7 +2333,7 @@ class sessionClass {
2347
2333
  break
2348
2334
  } else {
2349
2335
  if ( cache_data.items[i].title ) {
2350
- if ( (eventName == 'BIGINNING') && (cache_data.items[i].title == 'LIVE NOW: MLB Big Inning') ) {
2336
+ if ( (eventName == 'BIGINNING') && cache_data.items[i].title.startsWith('LIVE') && cache_data.items[i].title.includes('Big Inning') ) {
2351
2337
  this.debuglog('active big inning url')
2352
2338
  return cache_data.items[i].fields.url
2353
2339
  } else if ( cache_data.items[i].title.toUpperCase().endsWith(' VS. ' + eventName) ) {