mlbserver 2025.2.27 → 2025.3.2-9.2
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.
- package/index.js +86 -52
- package/package.json +1 -1
- package/session.js +42 -18
package/index.js
CHANGED
|
@@ -29,10 +29,10 @@ const VALID_CONTROLS = [ 'Show', 'Hide' ]
|
|
|
29
29
|
const VALID_INNING_HALF = [ '', 'top', 'bottom' ]
|
|
30
30
|
const VALID_INNING_NUMBER = [ '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
|
|
31
31
|
const VALID_SCORES = [ 'Hide', 'Show' ]
|
|
32
|
-
const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '
|
|
32
|
+
const VALID_RESOLUTIONS = [ 'adaptive', '1080p60', '720p60', '720p', '540p', '504p', '360p', '288p', '216p', '180p', 'none' ]
|
|
33
33
|
const DEFAULT_MULTIVIEW_RESOLUTION = '504p'
|
|
34
34
|
// Corresponding andwidths to display for above resolutions
|
|
35
|
-
const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '' ]
|
|
35
|
+
const DISPLAY_BANDWIDTHS = [ '', '9600k', '6600k', '4160k', '2950k', '2120k', '1400k', '1000k', '600k', '300k', '' ]
|
|
36
36
|
const VALID_AUDIO_TRACKS = [ 'all', 'English', 'Home Radio', 'Casa Radio', 'Away Radio', 'Visita Radio', 'Park', 'none' ]
|
|
37
37
|
const DISPLAY_AUDIO_TRACKS = [ 'all', 'TV', 'Radio', 'Spa.', 'Away Rad.', 'Away Sp.', 'Park', 'none' ]
|
|
38
38
|
const DEFAULT_MULTIVIEW_AUDIO_TRACK = 'English'
|
|
@@ -56,21 +56,42 @@ const GAMECHANGER_RESOLUTIONS = {
|
|
|
56
56
|
'frame_rate': '29.97',
|
|
57
57
|
'url_bandwidth': '1800',
|
|
58
58
|
'bandwidth': '2120',
|
|
59
|
-
'codec': '
|
|
59
|
+
'codec': '4d401f'
|
|
60
|
+
},
|
|
61
|
+
'180p': {
|
|
62
|
+
'resolution': '320x180',
|
|
63
|
+
'frame_rate': '29.97',
|
|
64
|
+
'url_bandwidth': '192',
|
|
65
|
+
'bandwidth': '600',
|
|
66
|
+
'codec': '4d401f'
|
|
67
|
+
},
|
|
68
|
+
'216p': {
|
|
69
|
+
'resolution': '384x216',
|
|
70
|
+
'frame_rate': '29.97',
|
|
71
|
+
'url_bandwidth': '450',
|
|
72
|
+
'bandwidth': '600',
|
|
73
|
+
'codec': '4d401f'
|
|
74
|
+
},
|
|
75
|
+
'288p': {
|
|
76
|
+
'resolution': '512x288',
|
|
77
|
+
'frame_rate': '29.97',
|
|
78
|
+
'url_bandwidth': '800',
|
|
79
|
+
'bandwidth': '1000',
|
|
80
|
+
'codec': '4d401f'
|
|
60
81
|
},
|
|
61
82
|
'360p': {
|
|
62
83
|
'resolution': '640x360',
|
|
63
84
|
'frame_rate': '29.97',
|
|
64
85
|
'url_bandwidth': '1200',
|
|
65
86
|
'bandwidth': '1400',
|
|
66
|
-
'codec': '
|
|
87
|
+
'codec': '4d401f'
|
|
67
88
|
},
|
|
68
89
|
'540p': {
|
|
69
90
|
'resolution': '960x540',
|
|
70
91
|
'frame_rate': '29.97',
|
|
71
92
|
'url_bandwidth': '2500',
|
|
72
93
|
'bandwidth': '2950',
|
|
73
|
-
'codec': '
|
|
94
|
+
'codec': '4d401f'
|
|
74
95
|
},
|
|
75
96
|
'720p': {
|
|
76
97
|
'resolution': '1280x720',
|
|
@@ -84,7 +105,14 @@ const GAMECHANGER_RESOLUTIONS = {
|
|
|
84
105
|
'frame_rate': '59.94',
|
|
85
106
|
'url_bandwidth': '5600',
|
|
86
107
|
'bandwidth': '6600',
|
|
87
|
-
'codec': '
|
|
108
|
+
'codec': '640029'
|
|
109
|
+
},
|
|
110
|
+
'1080p60': {
|
|
111
|
+
'resolution': '1920x1080',
|
|
112
|
+
'frame_rate': '59.94',
|
|
113
|
+
'url_bandwidth': '7500',
|
|
114
|
+
'bandwidth': '9600',
|
|
115
|
+
'codec': '64002a'
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
const GAMECHANGER_LIST_SIZE = 6
|
|
@@ -505,7 +533,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
505
533
|
return line
|
|
506
534
|
} else if ( segment_found ) {
|
|
507
535
|
segment_found = false
|
|
508
|
-
return http_root + '/ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
536
|
+
return http_root + '/segment.ts?url='+encodeURIComponent(url.resolve(streamURL, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
509
537
|
}
|
|
510
538
|
|
|
511
539
|
// Omit keyframe tracks
|
|
@@ -567,7 +595,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
567
595
|
//var parsed = line.match(/URI="([^"]+)"?$/)
|
|
568
596
|
var parsed = line.match(',URI="([^"]+)"')
|
|
569
597
|
if ( parsed[1] ) {
|
|
570
|
-
newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
|
|
598
|
+
newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim()))
|
|
571
599
|
if ( force_vod != VALID_FORCE_VOD[0] ) newurl += '&force_vod=on'
|
|
572
600
|
if ( inning_half != VALID_INNING_HALF[0] ) newurl += '&inning_half=' + inning_half
|
|
573
601
|
if ( inning_number != VALID_INNING_NUMBER[0] ) newurl += '&inning_number=' + inning_number
|
|
@@ -600,7 +628,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
600
628
|
if ( resolution == VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1] ) {
|
|
601
629
|
return
|
|
602
630
|
} else if ( resolution === VALID_RESOLUTIONS[0] ) {
|
|
603
|
-
|
|
631
|
+
return line
|
|
604
632
|
} else {
|
|
605
633
|
if ( (video_track_found == false) && (line.indexOf(resolution+',FRAME-RATE='+frame_rate) > 0) ) {
|
|
606
634
|
video_track_matched = true
|
|
@@ -621,7 +649,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
621
649
|
if ( line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="') ) {
|
|
622
650
|
var parsed = line.match(',URI="([^"]+)"')
|
|
623
651
|
if ( parsed[1] ) {
|
|
624
|
-
newurl = http_root + '/playlist?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
|
|
652
|
+
newurl = http_root + '/playlist.m3u8?url='+encodeURIComponent(url.resolve(streamURL, parsed[1].trim())) + content_protect + referer_parameter + token_parameter
|
|
625
653
|
return '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="' + newurl + '"'
|
|
626
654
|
}
|
|
627
655
|
return
|
|
@@ -643,7 +671,7 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
643
671
|
if ( gamePk ) newurl += '&gamePk=' + gamePk
|
|
644
672
|
if ( audio_track != VALID_AUDIO_TRACKS[0] ) newurl += '&audio_track=' + encodeURIComponent(audio_track)
|
|
645
673
|
newurl += content_protect + referer_parameter + token_parameter
|
|
646
|
-
return http_root + '/playlist?url='+newurl
|
|
674
|
+
return http_root + '/playlist.m3u8?url='+newurl
|
|
647
675
|
}
|
|
648
676
|
})
|
|
649
677
|
.filter(function(line) {
|
|
@@ -666,21 +694,21 @@ function getMasterPlaylist(streamURL, req, res, options = {}) {
|
|
|
666
694
|
|
|
667
695
|
|
|
668
696
|
// Listen for playlist requests
|
|
669
|
-
app.get('/playlist', async function(req, res) {
|
|
697
|
+
app.get('/playlist.m3u8', async function(req, res) {
|
|
670
698
|
if ( ! (await protect(req, res)) ) return
|
|
671
699
|
|
|
672
|
-
session.requestlog('playlist', req, true)
|
|
700
|
+
session.requestlog('playlist.m3u8', req, true)
|
|
673
701
|
|
|
674
702
|
delete req.headers.host
|
|
675
703
|
|
|
676
704
|
var u = req.query.url
|
|
677
|
-
session.debuglog('playlist url : ' + u)
|
|
705
|
+
session.debuglog('playlist.m3u8 url : ' + u)
|
|
678
706
|
|
|
679
707
|
var referer = false
|
|
680
708
|
var referer_parameter = ''
|
|
681
709
|
if ( req.query.referer ) {
|
|
682
710
|
referer = decodeURIComponent(req.query.referer)
|
|
683
|
-
session.debuglog('found playlist referer : ' + referer)
|
|
711
|
+
session.debuglog('found playlist.m3u8 referer : ' + referer)
|
|
684
712
|
referer_parameter = '&referer=' + encodeURIComponent(req.query.referer)
|
|
685
713
|
}
|
|
686
714
|
|
|
@@ -815,9 +843,9 @@ app.get('/playlist', async function(req, res) {
|
|
|
815
843
|
|
|
816
844
|
if (line[0] === '#') return line
|
|
817
845
|
|
|
818
|
-
let newline = http_root + '/ts'
|
|
846
|
+
let newline = http_root + '/segment.ts'
|
|
819
847
|
if ( line.includes('.vtt') ) {
|
|
820
|
-
newline = http_root + '/vtt'
|
|
848
|
+
newline = http_root + '/subtitles.vtt'
|
|
821
849
|
}
|
|
822
850
|
|
|
823
851
|
newline += '?url='+encodeURIComponent(url.resolve(u, line.trim())) + content_protect + referer_parameter + token_parameter
|
|
@@ -825,7 +853,7 @@ app.get('/playlist', async function(req, res) {
|
|
|
825
853
|
|
|
826
854
|
// if an alternate audio track is specified, force removal of embedded audio
|
|
827
855
|
if ( (audio_track != VALID_AUDIO_TRACKS[0]) && (audio_track != VALID_AUDIO_TRACKS[1]) ) {
|
|
828
|
-
newline = 'download.
|
|
856
|
+
newline = http_root + '/download.ts?audio_track=' + encodeURIComponent(audio_track) + '&src=' + encodeURIComponent('http://127.0.0.1:' + session.data.port + newline) + content_protect
|
|
829
857
|
}
|
|
830
858
|
|
|
831
859
|
return newline
|
|
@@ -869,20 +897,20 @@ app.get('/playlist', async function(req, res) {
|
|
|
869
897
|
})
|
|
870
898
|
|
|
871
899
|
// Listen for ts requests (video segments) and decode them
|
|
872
|
-
app.get('/ts', async function(req, res) {
|
|
900
|
+
app.get('/segment.ts', async function(req, res) {
|
|
873
901
|
if ( ! (await protect(req, res)) ) return
|
|
874
902
|
|
|
875
|
-
session.requestlog('ts', req, true)
|
|
903
|
+
session.requestlog('segment.ts', req, true)
|
|
876
904
|
|
|
877
905
|
delete req.headers.host
|
|
878
906
|
|
|
879
907
|
var u = req.query.url
|
|
880
|
-
session.debuglog('ts url : ' + u)
|
|
908
|
+
session.debuglog('segment.ts url : ' + u)
|
|
881
909
|
|
|
882
910
|
var headers = {encoding:null}
|
|
883
911
|
|
|
884
912
|
if ( req.query.referer ) {
|
|
885
|
-
session.debuglog('found segment referer : ' + req.query.referer)
|
|
913
|
+
session.debuglog('found segment.ts referer : ' + req.query.referer)
|
|
886
914
|
referer = decodeURIComponent(req.query.referer)
|
|
887
915
|
headers.referer = referer
|
|
888
916
|
headers.origin = getOriginFromURL(referer)
|
|
@@ -931,20 +959,20 @@ app.get('/ts', async function(req, res) {
|
|
|
931
959
|
|
|
932
960
|
|
|
933
961
|
// Listen for WebVTT subtitle requests
|
|
934
|
-
app.get('/vtt', async function(req, res) {
|
|
962
|
+
app.get('/subtitles.vtt', async function(req, res) {
|
|
935
963
|
if ( ! (await protect(req, res)) ) return
|
|
936
964
|
|
|
937
|
-
session.requestlog('vtt', req, true)
|
|
965
|
+
session.requestlog('subtitles.vtt', req, true)
|
|
938
966
|
|
|
939
967
|
delete req.headers.host
|
|
940
968
|
|
|
941
969
|
var u = req.query.url
|
|
942
|
-
session.debuglog('vtt url : ' + u)
|
|
970
|
+
session.debuglog('subtitles.vtt url : ' + u)
|
|
943
971
|
|
|
944
972
|
var referer = false
|
|
945
973
|
if ( req.query.referer ) {
|
|
946
974
|
referer = decodeURIComponent(req.query.referer)
|
|
947
|
-
session.debuglog('found vtt referer : ' + referer)
|
|
975
|
+
session.debuglog('found subtitles.vtt referer : ' + referer)
|
|
948
976
|
}
|
|
949
977
|
|
|
950
978
|
var req = function () {
|
|
@@ -1173,7 +1201,7 @@ app.get('/gamechangerplaylist', async function(req, res) {
|
|
|
1173
1201
|
if ( session.temp_cache.gamechanger[id].segments[i].discontinuity ) {
|
|
1174
1202
|
session.temp_cache.gamechanger[id].playlist[resolution] += '#EXT-X-DISCONTINUITY' + '\n'
|
|
1175
1203
|
}
|
|
1176
|
-
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
|
|
1204
|
+
session.temp_cache.gamechanger[id].playlist[resolution] += session.temp_cache.gamechanger[id].segments[i].extinf + '\n' + '/segment.ts?url=' + encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].ts) + '&streamURLToken='+encodeURIComponent(session.temp_cache.gamechanger[id].segments[i].streamURLToken) + content_protect + '\n'
|
|
1177
1205
|
}
|
|
1178
1206
|
|
|
1179
1207
|
session.debuglog(game_changer_title + 'playlist ' + session.temp_cache.gamechanger[id].playlist[resolution])
|
|
@@ -1523,6 +1551,8 @@ app.get('/', async function(req, res) {
|
|
|
1523
1551
|
let link = linkType.toLowerCase() + '.html'
|
|
1524
1552
|
if ( linkType == VALID_LINK_TYPES[1] ) {
|
|
1525
1553
|
link = linkType.toLowerCase() + '.m3u8'
|
|
1554
|
+
} else if ( linkType == VALID_LINK_TYPES[4] ) {
|
|
1555
|
+
link = linkType.toLowerCase() + '.ts'
|
|
1526
1556
|
} else {
|
|
1527
1557
|
force_vod = VALID_FORCE_VOD[0]
|
|
1528
1558
|
}
|
|
@@ -1574,7 +1604,7 @@ app.get('/', async function(req, res) {
|
|
|
1574
1604
|
body += '</td></tr>' + "\n"
|
|
1575
1605
|
}*/
|
|
1576
1606
|
|
|
1577
|
-
if (
|
|
1607
|
+
if ( cache_data.dates && cache_data.dates[0] && cache_data.dates[0].games && (cache_data.dates[0].games.length > 0) ) {
|
|
1578
1608
|
blackouts = await session.get_blackout_games(cache_data.dates[0].date, true)
|
|
1579
1609
|
}
|
|
1580
1610
|
|
|
@@ -1686,6 +1716,7 @@ app.get('/', async function(req, res) {
|
|
|
1686
1716
|
} else {
|
|
1687
1717
|
teams += hometeam
|
|
1688
1718
|
}
|
|
1719
|
+
let filename_teams = awayteam + " @ " + hometeam
|
|
1689
1720
|
let pitchers = ""
|
|
1690
1721
|
let state = "<br/>"
|
|
1691
1722
|
|
|
@@ -1764,7 +1795,7 @@ app.get('/', async function(req, res) {
|
|
|
1764
1795
|
state += "<br/>" + detailedState
|
|
1765
1796
|
}
|
|
1766
1797
|
|
|
1767
|
-
var filename = gameDate + ' ' +
|
|
1798
|
+
var filename = gameDate + ' ' + filename_teams + ' '
|
|
1768
1799
|
|
|
1769
1800
|
if ( cache_data.dates[0].games[j].doubleHeader != 'N' ) {
|
|
1770
1801
|
state += "<br/>Game " + cache_data.dates[0].games[j].gameNumber
|
|
@@ -1844,7 +1875,7 @@ app.get('/', async function(req, res) {
|
|
|
1844
1875
|
body += '><td>' + description + teams + pitchers + state + '</td>'
|
|
1845
1876
|
|
|
1846
1877
|
// Check if Winter League / MiLB game first
|
|
1847
|
-
if ( (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1878
|
+
if ( (cache_data.dates[0].games[j].teams['away'].team.sport.id != levels['MLB']) && (cache_data.dates[0].games[j].teams['home'].team.sport.id != levels['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1848
1879
|
body += "<td>"
|
|
1849
1880
|
if ( cache_data.dates[0].games[j].broadcasts ) {
|
|
1850
1881
|
let broadcastName = 'N/A'
|
|
@@ -1940,9 +1971,12 @@ app.get('/', async function(req, res) {
|
|
|
1940
1971
|
|
|
1941
1972
|
// display blackout tooltip, if necessary
|
|
1942
1973
|
if ( blackouts[gamePk] ) {
|
|
1943
|
-
body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
|
|
1944
|
-
if ( blackouts[gamePk].
|
|
1945
|
-
body += '
|
|
1974
|
+
body += '<span class="tooltip"><span class="blackout">' + teamabbr + '</span><span class="tooltiptext">' + blackouts[gamePk].blackout_type
|
|
1975
|
+
if ( blackouts[gamePk].blackout_type != 'Not entitled' ) {
|
|
1976
|
+
body += ' video blackout until approx. 2.5 hours after the game'
|
|
1977
|
+
if ( blackouts[gamePk].blackoutExpiry ) {
|
|
1978
|
+
body += ' (~' + blackouts[gamePk].blackoutExpiry.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }) + ')'
|
|
1979
|
+
}
|
|
1946
1980
|
}
|
|
1947
1981
|
body += '</span></span>'
|
|
1948
1982
|
} else {
|
|
@@ -2059,7 +2093,7 @@ app.get('/', async function(req, res) {
|
|
|
2059
2093
|
body += "</table>" + "\n"
|
|
2060
2094
|
|
|
2061
2095
|
if ( (Object.keys(blackouts).length > 0) ) {
|
|
2062
|
-
body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
|
|
2096
|
+
body += '<span class="tooltip tinytext"><span class="blackout">strikethrough</span> indicates a live blackout or non-entitled video<span class="tooltiptext">Tap or hover over the team abbreviation to see an estimate of when the blackout will be lifted (officially ~90 minutes, but more likely ~150 minutes or ~2.5 hours after the game ends).</span></span>' + "\n"
|
|
2063
2097
|
if ( argv.free ) {
|
|
2064
2098
|
body += '<br/>'
|
|
2065
2099
|
}
|
|
@@ -2132,7 +2166,7 @@ app.get('/', async function(req, res) {
|
|
|
2132
2166
|
body += '<input type="checkbox" id="reencode"/> <span class="tooltip">Re-encode all audio<span class="tooltiptext">Uses more CPU. Generally only necessary if you need the multiview stream to continue after one of the individual streams has ended. (Any streams with sync adjustments above will automatically be re-encoded, regardless of this setting.)</span></span><br/>' + "\n"
|
|
2133
2167
|
body += '<input type="checkbox" id="park_audio"/> <span class="tooltip">Park audio: filter out announcers<span class="tooltiptext">Implies re-encoding all audio. If this is enabled, an extra audio filter is applied to remove the announcer voices.</span></span><br/>' + "\n"
|
|
2134
2168
|
body += '<hr><span class="tooltip">Alternate audio URL and sync<span class="tooltiptext">Optional: you can also include a separate audio-only URL as an additional alternate audio track. Archive games will likely require a very large negative sync value, as the radio broadcasts may not be trimmed like the video archives.</span></span>:<br/><textarea id="audio_url" rows=2 cols=60 oninput="this.value=stream_substitution(this.value)"></textarea><input id="audio_url_seek" type="number" value="0" style="vertical-align:top;font-size:.8em;width:4em"/>'
|
|
2135
|
-
body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.
|
|
2169
|
+
body += '<hr>Watch: <a href="' + http_root + '/embed.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Embed</a> | <a href="' + http_root + '/stream.m3u8?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Stream</a> | <a href="' + http_root + '/chromecast.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Chromecast</a> | <a href="' + http_root + '/advanced.html?msrc=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Advanced</a> | <a href="' + http_root + '/download.ts?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '&filename=' + gameDate + ' Multiview">Download</a><br/><span class="tinytext">Kodi STRM files: <a href="' + http_root + '/kodi.strm?src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Matrix/19+</a> (<a href="' + http_root + '/kodi.strm?version=18&src=' + encodeURIComponent(multiview_stream_url) + content_protect_b + '">Leia/18</a>)</span>'
|
|
2136
2170
|
body += '</td></tr></table><br/>' + "\n"
|
|
2137
2171
|
}
|
|
2138
2172
|
|
|
@@ -2988,13 +3022,13 @@ app.get('/kodi.strm', async function(req, res) {
|
|
|
2988
3022
|
})
|
|
2989
3023
|
|
|
2990
3024
|
// Listen for download requests (either for actual downloads or just proxying stream through ffmpeg)
|
|
2991
|
-
app.get('/download.
|
|
3025
|
+
app.get('/download.ts', async function(req, res) {
|
|
2992
3026
|
if ( ! (await protect(req, res)) ) return
|
|
2993
3027
|
|
|
2994
3028
|
try {
|
|
2995
3029
|
// we'll know it's an actual download request if it include a filename parameter
|
|
2996
3030
|
if ( req.query.filename ) {
|
|
2997
|
-
session.requestlog('download', req)
|
|
3031
|
+
session.requestlog('download.ts', req)
|
|
2998
3032
|
} else {
|
|
2999
3033
|
session.debuglog('force alternate audio', req)
|
|
3000
3034
|
}
|
|
@@ -3011,7 +3045,7 @@ app.get('/download.html', async function(req, res) {
|
|
|
3011
3045
|
}
|
|
3012
3046
|
video_url = server + video_url
|
|
3013
3047
|
}
|
|
3014
|
-
session.debuglog('download src : ' + video_url)
|
|
3048
|
+
session.debuglog('download.ts src : ' + video_url)
|
|
3015
3049
|
|
|
3016
3050
|
// force adaptive streams to just use a single video resolution/track
|
|
3017
3051
|
if ( req.query.filename ) {
|
|
@@ -3033,7 +3067,7 @@ app.get('/download.html', async function(req, res) {
|
|
|
3033
3067
|
// video
|
|
3034
3068
|
if ( !req.query.resolution || (req.query.resolution != VALID_RESOLUTIONS[VALID_RESOLUTIONS.length-1]) ) {
|
|
3035
3069
|
// copy first video track if available
|
|
3036
|
-
ffmpeg_command.addOutputOption('-map', '0:v:0
|
|
3070
|
+
ffmpeg_command.addOutputOption('-map', '0:v:0')
|
|
3037
3071
|
.addOutputOption('-c:v', 'copy')
|
|
3038
3072
|
} else {
|
|
3039
3073
|
// suppress video is "none" resolution was specified
|
|
@@ -3058,13 +3092,6 @@ app.get('/download.html', async function(req, res) {
|
|
|
3058
3092
|
ffmpeg_command.addOutputOption('-map', '[out0]')
|
|
3059
3093
|
.addOutputOption('-c:a', 'aac')
|
|
3060
3094
|
.addOutputOption('-ac:a:0', '1')
|
|
3061
|
-
|
|
3062
|
-
// if not downloading to a file, also copy source PTS values for continuous playback
|
|
3063
|
-
if ( !req.query.filename ) {
|
|
3064
|
-
ffmpeg_command.addOutputOption('-copyts')
|
|
3065
|
-
.addOutputOption('-muxpreload', '0')
|
|
3066
|
-
.addOutputOption('-muxdelay', '0')
|
|
3067
|
-
}
|
|
3068
3095
|
} else if ( (!req.query.filename && (req.query.audio_track != VALID_AUDIO_TRACKS[1])) || (req.query.audio_track == VALID_AUDIO_TRACKS[7]) ) {
|
|
3069
3096
|
// if we're not downloading a file, and we requested an alternate audio track, then we want to suppress the embedded TV audio
|
|
3070
3097
|
// or if the user requested no audio tracks in their download, we will suppress all
|
|
@@ -3076,22 +3103,29 @@ app.get('/download.html', async function(req, res) {
|
|
|
3076
3103
|
}
|
|
3077
3104
|
}
|
|
3078
3105
|
|
|
3106
|
+
// if not downloading to a file, also copy source PTS values for continuous playback
|
|
3107
|
+
if ( !req.query.filename ) {
|
|
3108
|
+
ffmpeg_command.addOutputOption('-copyts')
|
|
3109
|
+
.addOutputOption('-muxpreload', '0')
|
|
3110
|
+
.addOutputOption('-muxdelay', '0')
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3079
3113
|
// output mpegts to response stream
|
|
3080
3114
|
ffmpeg_command.addOutputOption('-f', 'mpegts')
|
|
3081
3115
|
.output(res)
|
|
3082
3116
|
.on('start', function(commandLine) {
|
|
3083
|
-
session.debuglog('download command started')
|
|
3117
|
+
session.debuglog('download.ts command started')
|
|
3084
3118
|
if ( argv.debug || argv.ffmpeg_logging ) {
|
|
3085
|
-
session.debuglog('download command: ' + commandLine)
|
|
3119
|
+
session.debuglog('download.ts command: ' + commandLine)
|
|
3086
3120
|
}
|
|
3087
3121
|
})
|
|
3088
3122
|
.on('error', function(err, stdout, stderr) {
|
|
3089
|
-
session.debuglog('download command stopped: ' + err.message)
|
|
3123
|
+
session.debuglog('download.ts command stopped: ' + err.message)
|
|
3090
3124
|
if ( stdout ) session.log(stdout)
|
|
3091
3125
|
if ( stderr ) session.log(stderr)
|
|
3092
3126
|
})
|
|
3093
3127
|
.on('end', function() {
|
|
3094
|
-
session.debuglog('download command ended')
|
|
3128
|
+
session.debuglog('download.ts command ended')
|
|
3095
3129
|
})
|
|
3096
3130
|
|
|
3097
3131
|
if ( argv.ffmpeg_logging ) {
|
|
@@ -3109,7 +3143,7 @@ app.get('/download.html', async function(req, res) {
|
|
|
3109
3143
|
|
|
3110
3144
|
ffmpeg_command.run()
|
|
3111
3145
|
} catch (e) {
|
|
3112
|
-
session.log('download request error : ' + e.message)
|
|
3146
|
+
session.log('download.ts request error : ' + e.message)
|
|
3113
3147
|
res.end('')
|
|
3114
3148
|
}
|
|
3115
|
-
})
|
|
3149
|
+
})
|
package/package.json
CHANGED
package/session.js
CHANGED
|
@@ -38,10 +38,14 @@ const WINTER_LEAGUES = [AFL_ID, LIDOM_ID]
|
|
|
38
38
|
const BREAK_TYPES = ['Game Advisory', 'Pitching Substitution', 'Offensive Substitution', 'Defensive Sub', 'Defensive Switch', 'Runner Placed On Base']
|
|
39
39
|
// These are the events to keep, in addition to the last event of each at-bat, if we're skipping pitches
|
|
40
40
|
const ACTION_TYPES = ['Wild Pitch', 'Passed Ball', 'Stolen Base', 'Caught Stealing', 'Pickoff', 'Error', 'Out', 'Balk', 'Defensive Indiff', 'Other Advance']
|
|
41
|
+
// These are some idle events to skip
|
|
42
|
+
const IDLE_TYPES = ['Mound Visit', 'Batter Timeout', 'Pitcher Step Off']
|
|
41
43
|
const EVENT_START_PADDING = -3
|
|
42
|
-
const PITCH_END_PADDING =
|
|
44
|
+
const PITCH_END_PADDING = 2
|
|
43
45
|
const ACTION_END_PADDING = 7
|
|
44
46
|
const MINIMUM_BREAK_DURATION = 5
|
|
47
|
+
// extra padding for MLB events (2025)
|
|
48
|
+
const MLB_PADDING = 39
|
|
45
49
|
|
|
46
50
|
const LI_TABLE = {
|
|
47
51
|
1: {
|
|
@@ -1776,7 +1780,7 @@ class sessionClass {
|
|
|
1776
1780
|
if ( ((team.toUpperCase() == home_team) && (LEVELS[level.toUpperCase()] == home_level)) || ((team.toUpperCase() == away_team) && (LEVELS[level.toUpperCase()] == away_level)) || ((team.toUpperCase().indexOf('NATIONAL.') == 0) && (home_level == LEVELS['MLB'])) || ((team.toUpperCase().startsWith('FREE.') && cache_data.dates[0].games[j].broadcasts && cache_data.dates[0].games[j].broadcasts[0] && (cache_data.dates[0].games[j].broadcasts[0].freeGame == true))) ) {
|
|
1777
1781
|
|
|
1778
1782
|
// Check if Winter League / MiLB game first
|
|
1779
|
-
if ( (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1783
|
+
if ( (away_level != LEVELS['MLB']) && (home_level != LEVELS['MLB']) && (mediaType == 'MLBTV') ) {
|
|
1780
1784
|
this.debuglog('matched non-MLB team for ' + cache_data.dates[0].games[j].teams['home'].team.abbreviation + '@' + cache_data.dates[0].games[j].teams['away'].team.abbreviation)
|
|
1781
1785
|
if ( cache_data.dates[0].games[j].broadcasts ) {
|
|
1782
1786
|
let broadcastName = 'N/A'
|
|
@@ -2892,8 +2896,14 @@ class sessionClass {
|
|
|
2892
2896
|
async getBroadcastStart(streamURL, streamURLToken) {
|
|
2893
2897
|
try {
|
|
2894
2898
|
this.debuglog('getBroadcastStart')
|
|
2899
|
+
|
|
2900
|
+
// MLB version
|
|
2901
|
+
let variant = '_5600K'
|
|
2902
|
+
if ( streamURL.includes('milb.com') ) {
|
|
2903
|
+
variant = '_1280x720_59_5472K'
|
|
2904
|
+
}
|
|
2895
2905
|
|
|
2896
|
-
let variant_url = 'http://localhost:' + this.data.port + '/playlist?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + '
|
|
2906
|
+
let variant_url = 'http://localhost:' + this.data.port + '/playlist.m3u8?url=' + encodeURIComponent(streamURL.substr(0,streamURL.length-5) + variant + '.m3u8')
|
|
2897
2907
|
if ( streamURLToken ) {
|
|
2898
2908
|
variant_url += '&streamURLToken=' + encodeURIComponent(streamURLToken)
|
|
2899
2909
|
}
|
|
@@ -2929,6 +2939,15 @@ class sessionClass {
|
|
|
2929
2939
|
this.debuglog('getSkipMarkers')
|
|
2930
2940
|
|
|
2931
2941
|
if ( skip_adjust != 0 ) this.log('manual adjustment of ' + skip_adjust + ' seconds being applied')
|
|
2942
|
+
|
|
2943
|
+
let event_start_padding = EVENT_START_PADDING
|
|
2944
|
+
let pitch_end_padding = PITCH_END_PADDING
|
|
2945
|
+
let action_end_padding = ACTION_END_PADDING
|
|
2946
|
+
if ( !streamURL.includes('milb.com') ) {
|
|
2947
|
+
event_start_padding += MLB_PADDING
|
|
2948
|
+
pitch_end_padding += MLB_PADDING
|
|
2949
|
+
action_end_padding += MLB_PADDING
|
|
2950
|
+
}
|
|
2932
2951
|
|
|
2933
2952
|
if ( !this.temp_cache[gamePk] ) {
|
|
2934
2953
|
this.temp_cache[gamePk] = {}
|
|
@@ -2993,7 +3012,7 @@ class sessionClass {
|
|
|
2993
3012
|
if ((current_inning > start_inning) || ((current_inning == start_inning) && ((current_inning_half == start_inning_half) || (current_inning_half == 'bottom')))) {
|
|
2994
3013
|
// loop through events within each play
|
|
2995
3014
|
for (var j=0; j < cache_data.liveData.plays.allPlays[i].playEvents.length; j++) {
|
|
2996
|
-
let event_end_padding =
|
|
3015
|
+
let event_end_padding = action_end_padding
|
|
2997
3016
|
// always exclude break types
|
|
2998
3017
|
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)) {
|
|
2999
3018
|
// if we're in the process of skipping inning breaks, treat the first break type we find as another inning break
|
|
@@ -3004,15 +3023,18 @@ class sessionClass {
|
|
|
3004
3023
|
continue
|
|
3005
3024
|
} else {
|
|
3006
3025
|
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))) ) {
|
|
3007
|
-
event_end_padding =
|
|
3026
|
+
event_end_padding = pitch_end_padding
|
|
3008
3027
|
}
|
|
3009
3028
|
let action_index
|
|
3010
|
-
// skip type 1 (breaks)
|
|
3011
|
-
if ((skip_type
|
|
3029
|
+
// skip type 1 (breaks) will look at all plays with an endTime
|
|
3030
|
+
if ((skip_type == 1) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime) {
|
|
3031
|
+
action_index = j
|
|
3032
|
+
// skip type 2 (idle time) will look at all non-idle plays with an endTime
|
|
3033
|
+
} else if ((skip_type == 2) && cache_data.liveData.plays.allPlays[i].playEvents[j].endTime && !IDLE_TYPES.some(v => cache_data.liveData.plays.allPlays[i].playEvents[j].details.description.includes(v))) {
|
|
3012
3034
|
action_index = j
|
|
3013
3035
|
} else if (skip_type == 3) {
|
|
3014
3036
|
// skip type 3 excludes non-action pitches (events that aren't last in the at-bat and don't fall under action types)
|
|
3015
|
-
if ( event_end_padding ==
|
|
3037
|
+
if ( event_end_padding == pitch_end_padding ) {
|
|
3016
3038
|
continue
|
|
3017
3039
|
} else {
|
|
3018
3040
|
// if the action is associated with another play or the event doesn't have an end time, use the previous event instead
|
|
@@ -3026,7 +3048,7 @@ class sessionClass {
|
|
|
3026
3048
|
if (typeof action_index === 'undefined') {
|
|
3027
3049
|
continue
|
|
3028
3050
|
} else {
|
|
3029
|
-
let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) +
|
|
3051
|
+
let break_end = ((new Date(cache_data.liveData.plays.allPlays[i].playEvents[action_index].startTime) - broadcast_start_timestamp) / 1000) + event_start_padding
|
|
3030
3052
|
|
|
3031
3053
|
// attempt to fix erroneous timestamps, like NYY-SEA 2022-08-09, bottom 11
|
|
3032
3054
|
if ( break_end < break_start ) {
|
|
@@ -3583,25 +3605,27 @@ class sessionClass {
|
|
|
3583
3605
|
let game = cache_data.results[j]
|
|
3584
3606
|
let game_pk = game.gamePk
|
|
3585
3607
|
this.debuglog('get_blackout_games checking game ' + game_pk)
|
|
3586
|
-
if ( game.blackedOutVideo ) {
|
|
3587
|
-
this.debuglog('get_blackout_games found blackout')
|
|
3608
|
+
if ( game.blackedOutVideo || !game.entitledVideo ) {
|
|
3609
|
+
this.debuglog('get_blackout_games found blackout or non-entitled video')
|
|
3588
3610
|
let blackout_type = ''
|
|
3589
|
-
|
|
3590
|
-
/*if ( game.videoStatusCodes.includes('2') ) {
|
|
3611
|
+
if ( game.videoStatusCodes.includes(2) ) {
|
|
3591
3612
|
this.debuglog('get_blackout_games found national blackout')
|
|
3592
3613
|
blackout_type = 'National/International'
|
|
3593
|
-
} else {
|
|
3614
|
+
} else if ( game.videoStatusCodes.includes(1) ) {
|
|
3594
3615
|
this.debuglog('get_blackout_games found local blackout')
|
|
3595
3616
|
blackout_type = 'Local'
|
|
3596
|
-
}
|
|
3617
|
+
} else {
|
|
3618
|
+
this.debuglog('get_blackout_games found non-entitled video')
|
|
3619
|
+
blackout_type = 'Not entitled'
|
|
3620
|
+
}
|
|
3597
3621
|
blackouts[game_pk] = { blackout_type: blackout_type }
|
|
3598
|
-
} else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
|
|
3622
|
+
} /*else if ( !game.entitledVideo && (game.videoStatusCodes[0] == '3') ) {
|
|
3599
3623
|
this.debuglog('get_blackout_games found non-entitled MVPD required blackout')
|
|
3600
3624
|
blackouts[game_pk] = { blackout_type: '' }
|
|
3601
|
-
}
|
|
3625
|
+
}*/
|
|
3602
3626
|
|
|
3603
3627
|
// add blackout expiry, if requested
|
|
3604
|
-
if ( blackouts[game_pk] && calculate_expiries && await this.check_game_time(game.gameData) ) {
|
|
3628
|
+
if ( blackouts[game_pk] && (blackouts[game_pk].blackout_type != 'Not entitled') && calculate_expiries && await this.check_game_time(game.gameData) ) {
|
|
3605
3629
|
this.debuglog('get_blackout_games calculating blackout expiry')
|
|
3606
3630
|
let date_cache_data = await this.getDayData(gameDate)
|
|
3607
3631
|
if ( date_cache_data.dates && date_cache_data.dates[0] && date_cache_data.dates[0].games && (date_cache_data.dates[0].games.length > 0) ) {
|