@@ -38,6 +38,7 @@ goog.require('shaka.util.OperationManager');
38
38
goog . require ( 'shaka.util.Pssh' ) ;
39
39
goog . require ( 'shaka.util.Timer' ) ;
40
40
goog . require ( 'shaka.util.Platform' ) ;
41
+ goog . require ( 'shaka.util.XmlUtils' ) ;
41
42
goog . requireType ( 'shaka.hls.Segment' ) ;
42
43
43
44
@@ -120,6 +121,15 @@ shaka.hls.HlsParser = class {
120
121
*/
121
122
this . updatePlaylistDelay_ = 0 ;
122
123
124
+ /**
125
+ * A time offset to apply to EXT-X-PROGRAM-DATE-TIME values to normalize
126
+ * them so that they start at 0. This is necessary because these times will
127
+ * be used to set presentation times for segments.
128
+ * null means we don't have enough data yet.
129
+ * @private {?number}
130
+ */
131
+ this . syncTimeOffset_ = null ;
132
+
123
133
/**
124
134
* This timer is used to trigger the start of a manifest update. A manifest
125
135
* update is async. Once the update is finished, the timer will be restarted
@@ -351,6 +361,42 @@ shaka.hls.HlsParser = class {
351
361
// No-op
352
362
}
353
363
364
+ /**
365
+ * If necessary, makes sure that sync times will be normalized to 0, so that
366
+ * a stream does not start buffering at 50 years in because sync times are
367
+ * measured in time since 1970.
368
+ * @private
369
+ */
370
+ calculateSyncTimeOffset_ ( ) {
371
+ if ( this . syncTimeOffset_ != null ) {
372
+ // The offset was already calculated.
373
+ return ;
374
+ }
375
+
376
+ const segments = new Set ( ) ;
377
+ let lowestSyncTime = Infinity ;
378
+ for ( const streamInfo of this . uriToStreamInfosMap_ . values ( ) ) {
379
+ const segmentIndex = streamInfo . stream . segmentIndex ;
380
+ if ( segmentIndex ) {
381
+ segmentIndex . forEachTopLevelReference ( ( segment ) => {
382
+ if ( segment . syncTime != null ) {
383
+ lowestSyncTime = Math . min ( lowestSyncTime , segment . syncTime ) ;
384
+ segments . add ( segment ) ;
385
+ }
386
+ } ) ;
387
+ }
388
+ }
389
+ if ( segments . size > 0 ) {
390
+ this . syncTimeOffset_ = - lowestSyncTime ;
391
+ for ( const segment of segments ) {
392
+ segment . syncTime += this . syncTimeOffset_ ;
393
+ for ( const partial of segment . partialReferences ) {
394
+ partial . syncTime += this . syncTimeOffset_ ;
395
+ }
396
+ }
397
+ }
398
+ }
399
+
354
400
/**
355
401
* Parses the manifest.
356
402
*
@@ -433,6 +479,10 @@ shaka.hls.HlsParser = class {
433
479
shaka . util . Error . Code . OPERATION_ABORTED ) ;
434
480
}
435
481
482
+ // Now that we have generated all streams, we can determine the offset to
483
+ // apply to sync times.
484
+ this . calculateSyncTimeOffset_ ( ) ;
485
+
436
486
if ( this . aesEncrypted_ && variants . length == 0 ) {
437
487
// We do not support AES-128 encryption with HLS yet. Variants is null
438
488
// when the playlist is encrypted with AES-128.
@@ -1706,8 +1756,8 @@ shaka.hls.HlsParser = class {
1706
1756
* @param {number } startTime
1707
1757
* @param {!Map.<string, string> } variables
1708
1758
* @param {string } absoluteMediaPlaylistUri
1709
- * @return {!shaka.media.SegmentReference }
1710
1759
* @param {string } type
1760
+ * @return {!shaka.media.SegmentReference }
1711
1761
* @private
1712
1762
*/
1713
1763
createSegmentReference_ (
@@ -1730,6 +1780,23 @@ shaka.hls.HlsParser = class {
1730
1780
'true, and see https://bit.ly/3clctcj for details.' ) ;
1731
1781
}
1732
1782
1783
+ let syncTime = null ;
1784
+ if ( ! this . config_ . hls . ignoreManifestProgramDateTime ) {
1785
+ const dateTimeTag =
1786
+ shaka . hls . Utils . getFirstTagWithName ( tags , 'EXT-X-PROGRAM-DATE-TIME' ) ;
1787
+ if ( dateTimeTag && dateTimeTag . value ) {
1788
+ const time = shaka . util . XmlUtils . parseDate ( dateTimeTag . value ) ;
1789
+ goog . asserts . assert ( time != null ,
1790
+ 'EXT-X-PROGRAM-DATE-TIME format not valid' ) ;
1791
+ // Sync time offset is null on the first go-through. This indicates that
1792
+ // we have not yet seen every stream, and thus do not yet have enough
1793
+ // information to determine how to normalize the sync times.
1794
+ // For that first go-through, the sync time will be applied after the
1795
+ // references are all created. Until then, just offset by 0.
1796
+ syncTime = time + ( this . syncTimeOffset_ || 0 ) ;
1797
+ }
1798
+ }
1799
+
1733
1800
// Create SegmentReferences for the partial segments.
1734
1801
const partialSegmentRefs = [ ] ;
1735
1802
if ( this . lowLatencyMode_ && hlsSegment . partialSegments . length ) {
@@ -1839,6 +1906,7 @@ shaka.hls.HlsParser = class {
1839
1906
partialSegmentRefs ,
1840
1907
tilesLayout ,
1841
1908
tileDuration ,
1909
+ syncTime ,
1842
1910
) ;
1843
1911
}
1844
1912
@@ -1966,6 +2034,81 @@ shaka.hls.HlsParser = class {
1966
2034
references . push ( reference ) ;
1967
2035
}
1968
2036
2037
+ // If some segments have sync times, but not all, extrapolate the sync
2038
+ // times of the ones with none.
2039
+ const someSyncTime = references . some ( ( ref ) => ref . syncTime != null ) ;
2040
+ if ( someSyncTime ) {
2041
+ for ( let i = 0 ; i < references . length ; i ++ ) {
2042
+ const reference = references [ i ] ;
2043
+ if ( reference . syncTime != null ) {
2044
+ // No need to extrapolate.
2045
+ continue ;
2046
+ }
2047
+ // Find the nearest segment with syncTime, in either direction.
2048
+ // This looks forward and backward simultaneously, keeping track of what
2049
+ // to offset the syncTime it finds by as it goes.
2050
+ let forwardAdd = 0 ;
2051
+ let forwardI = i ;
2052
+ /**
2053
+ * Look forwards one reference at a time, summing all durations as we
2054
+ * go, until we find a reference with a syncTime to use as a basis.
2055
+ * This DOES count the original reference, but DOESN'T count the first
2056
+ * reference with a syncTime (as we approach it from behind).
2057
+ * @return {?number }
2058
+ */
2059
+ const lookForward = ( ) => {
2060
+ const other = references [ forwardI ] ;
2061
+ if ( other ) {
2062
+ if ( other . syncTime != null ) {
2063
+ return other . syncTime + forwardAdd ;
2064
+ }
2065
+ forwardAdd -= other . endTime - other . startTime ;
2066
+ forwardI += 1 ;
2067
+ }
2068
+ return null ;
2069
+ } ;
2070
+ let backwardAdd = 0 ;
2071
+ let backwardI = i ;
2072
+ /**
2073
+ * Look backwards one reference at a time, summing all durations as we
2074
+ * go, until we find a reference with a syncTime to use as a basis.
2075
+ * This DOESN'T count the original reference, but DOES count the first
2076
+ * reference with a syncTime (as we approach it from ahead).
2077
+ * @return {?number }
2078
+ */
2079
+ const lookBackward = ( ) => {
2080
+ const other = references [ backwardI ] ;
2081
+ if ( other ) {
2082
+ if ( other != reference ) {
2083
+ backwardAdd += other . endTime - other . startTime ;
2084
+ }
2085
+ if ( other . syncTime != null ) {
2086
+ return other . syncTime + backwardAdd ;
2087
+ }
2088
+ backwardI -= 1 ;
2089
+ }
2090
+ return null ;
2091
+ } ;
2092
+ while ( reference . syncTime == null ) {
2093
+ reference . syncTime = lookBackward ( ) ;
2094
+ if ( reference . syncTime == null ) {
2095
+ reference . syncTime = lookForward ( ) ;
2096
+ }
2097
+ }
2098
+ }
2099
+ }
2100
+
2101
+ // Split the sync times properly among partial segments.
2102
+ if ( someSyncTime ) {
2103
+ for ( const reference of references ) {
2104
+ let syncTime = reference . syncTime ;
2105
+ for ( const partial of reference . partialReferences ) {
2106
+ partial . syncTime = syncTime ;
2107
+ syncTime += partial . endTime - partial . startTime ;
2108
+ }
2109
+ }
2110
+ }
2111
+
1969
2112
return references ;
1970
2113
}
1971
2114
0 commit comments