Skip to content

Commit

Permalink
fix: Fix PES rollover in TS (#6363)
Browse files Browse the repository at this point in the history
Related to #6320 (comment)
  • Loading branch information
avelad committed Apr 8, 2024
1 parent a134917 commit 4ec646b
Show file tree
Hide file tree
Showing 16 changed files with 141 additions and 4 deletions.
1 change: 1 addition & 0 deletions karma.conf.js
Expand Up @@ -252,6 +252,7 @@ module.exports = (config) => {
{pattern: 'test/test/assets/hls-ts-muxed-ac3-h264/*', included: false},
{pattern: 'test/test/assets/hls-ts-muxed-mp3-h264/*', included: false},
{pattern: 'test/test/assets/hls-ts-muxed-ec3-h264/*', included: false},
{pattern: 'test/test/assets/hls-ts-rollover/*', included: false},
{pattern: 'dist/shaka-player.ui.js', included: false},
{pattern: 'dist/locales.js', included: false},
{pattern: 'demo/**/*.js', included: false},
Expand Down
11 changes: 10 additions & 1 deletion lib/cea/ts_cea_parser.js
Expand Up @@ -26,6 +26,9 @@ shaka.cea.TsCeaParser = class {
* @const {!shaka.cea.SeiProcessor}
*/
this.seiProcessor_ = new shaka.cea.SeiProcessor();

/** @private {?shaka.util.TsParser} */
this.tsParser_ = null;
}

/**
Expand All @@ -41,14 +44,20 @@ shaka.cea.TsCeaParser = class {
parse(mediaSegment) {
const CeaUtils = shaka.cea.CeaUtils;

if (!this.tsParser_) {
this.tsParser_ = new shaka.util.TsParser();
} else {
this.tsParser_.clearData();
}

/** @type {!Array<!shaka.extern.ICeaParser.CaptionPacket>} **/
const captionPackets = [];

const uint8ArrayData = shaka.util.BufferUtils.toUint8(mediaSegment);
if (!shaka.util.TsParser.probe(uint8ArrayData)) {
return captionPackets;
}
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
const tsParser = this.tsParser_.parse(uint8ArrayData);
const codecs = tsParser.getCodecs();
const videoNalus = tsParser.getVideoNalus();
const validNaluTypes = [];
Expand Down
14 changes: 13 additions & 1 deletion lib/media/media_source_engine.js
Expand Up @@ -151,6 +151,9 @@ shaka.media.MediaSourceEngine = class {

/** @private {?number} */
this.lastDuration_ = null;

/** @private {?shaka.util.TsParser} */
this.tsParser_ = null;
}

/**
Expand Down Expand Up @@ -392,6 +395,8 @@ shaka.media.MediaSourceEngine = class {

// This object is owned by Player
this.lcevcDec_ = null;

this.tsParser_ = null;
}

/**
Expand Down Expand Up @@ -442,6 +447,8 @@ shaka.media.MediaSourceEngine = class {
this.manifestType_ == shaka.media.ManifestParser.HLS &&
!this.ignoreManifestTimestampsInSegmentsMode_;

this.tsParser_ = null;

for (const contentType of streamsByType.keys()) {
const stream = streamsByType.get(contentType);
this.initSourceBuffer_(contentType, stream, stream.codecs);
Expand Down Expand Up @@ -789,7 +796,12 @@ shaka.media.MediaSourceEngine = class {
}
} else if (!mimeType.includes('/mp4') && !mimeType.includes('/webm') &&
shaka.util.TsParser.probe(uint8ArrayData)) {
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
if (!this.tsParser_) {
this.tsParser_ = new shaka.util.TsParser();
} else {
this.tsParser_.clearData();
}
const tsParser = this.tsParser_.parse(uint8ArrayData);
const startTime = tsParser.getStartTime(contentType);
if (startTime != null) {
timestamp = startTime;
Expand Down
58 changes: 56 additions & 2 deletions lib/util/ts_parser.js
Expand Up @@ -55,6 +55,12 @@ shaka.util.TsParser = class {

/** @private {!Array.<!Array.<Uint8Array>>} */
this.id3Data_ = [];

/** @private {?number} */
this.referencePts_ = null;

/** @private {?number} */
this.referenceDts_ = null;
}

/**
Expand Down Expand Up @@ -390,22 +396,36 @@ shaka.util.TsParser = class {
// the PTS and DTS are not written out directly. For information
// on how they are encoded, see
// http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
pes.pts =
const pts =
(data[9] & 0x0e) * 536870912 + // 1 << 29
(data[10] & 0xff) * 4194304 + // 1 << 22
(data[11] & 0xfe) * 16384 + // 1 << 14
(data[12] & 0xff) * 128 + // 1 << 7
(data[13] & 0xfe) / 2;

if (this.referencePts_ == null) {
this.referencePts_ = pts;
}

pes.pts = this.handleRollover_(pts, this.referencePts_);
this.referencePts_ = pes.pts;

pes.dts = pes.pts;
if (ptsDtsFlags & 0x40) {
pes.dts =
const dts =
(data[14] & 0x0e) * 536870912 + // 1 << 29
(data[15] & 0xff) * 4194304 + // 1 << 22
(data[16] & 0xfe) * 16384 + // 1 << 14
(data[17] & 0xff) * 128 + // 1 << 7
(data[18] & 0xfe) / 2;

if (this.referenceDts_ == null) {
this.referenceDts_ = dts;
}

pes.dts = this.handleRollover_(dts, this.referenceDts_);
}
this.referenceDts_ = pes.dts;
}

const pesHdrLen = data[8];
Expand Down Expand Up @@ -1069,6 +1089,40 @@ shaka.util.TsParser = class {
return ('0' + x.toString(16).toUpperCase()).slice(-2);
}

/**
* @param {number} value
* @param {number} reference
* @return {number}
* @private
*/
handleRollover_(value, reference) {
const MAX_TS = 8589934592;
const RO_THRESH = 4294967296;

let direction = 1;

if (value > reference) {
// If the current timestamp value is greater than our reference timestamp
// and we detect a timestamp rollover, this means the roll over is
// happening in the opposite direction.
// Example scenario: Enter a long stream/video just after a rollover
// occurred. The reference point will be set to a small number, e.g. 1.
// The user then seeks backwards over the rollover point. In loading this
// segment, the timestamp values will be very large, e.g. 2^33 - 1. Since
// this comes before the data we loaded previously, we want to adjust the
// time stamp to be `value - 2^33`.
direction = -1;
}

// Note: A seek forwards or back that is greater than the RO_THRESH
// (2^32, ~13 hours) will cause an incorrect adjustment.
while (Math.abs(reference - value) > RO_THRESH) {
value += (direction * MAX_TS);
}

return value;
}

/**
* Check if the passed data corresponds to an MPEG2-TS
*
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
39 changes: 39 additions & 0 deletions test/test/assets/hls-ts-rollover/playlist.m3u8
@@ -0,0 +1,39 @@
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:17.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000024.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:19.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000025.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:21.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000026.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:23.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000027.ts

## Rollover starts in the middle of the segment 28.ts

#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:25.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000028.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:27.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000029.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:29.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000030.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:31.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000031.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:33.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000032.ts
#EXT-X-PROGRAM-DATE-TIME:2024-03-04T23:02:35.684Z
#EXTINF:2.000000
360p30_h264_48k_160_aac_ts-0000000033.ts
#EXT-X-ENDLIST
22 changes: 22 additions & 0 deletions test/transmuxer/transmuxer_integration.js
Expand Up @@ -326,6 +326,28 @@ describe('Transmuxer Player', () => {
await player.unload();
});

it('H.264+AAC in TS with rollover', async () => {
// eslint-disable-next-line max-len
await player.load('/base/test/test/assets/hls-ts-rollover/playlist.m3u8');
await video.play();
expect(player.isLive()).toBe(false);

// Wait for the video to start playback. If it takes longer than 10
// seconds, fail the test.
await waiter.waitForMovementOrFailOnTimeout(video, 10);

// The rollover occurs around the 9th second, without the rollover, the
// media source times are wrong and the stream freezes. The purpose is to
// play at least 15 seconds to see that the rollover passes and the
// stream continues without problems.

// Play for 15 seconds, but stop early if the video ends. If it takes
// longer than 45 seconds, fail the test.
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45);

await player.unload();
});

it('H.265+AAC in TS', async () => {
if (!isH265Supported()) {
pending('Codec H.265 is not supported by the platform.');
Expand Down

0 comments on commit 4ec646b

Please sign in to comment.