Skip to content

Commit 16583a4

Browse files
committed
#7064: Check integrity of MPEG-TS video stream.
1 parent ddb80e6 commit 16583a4

File tree

4 files changed

+110
-2
lines changed

4 files changed

+110
-2
lines changed

api-extractor/report/hls.js.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4886,6 +4886,7 @@ export interface TransmuxerResult {
48864886
// @public (undocumented)
48874887
export type TSDemuxerConfig = {
48884888
forceKeyFrameOnDiscontinuity: boolean;
4889+
handleMpegTsVideoIntegrityErrors: 'process' | 'skip';
48894890
};
48904891

48914892
// Warning: (ae-missing-release-tag) "UriReplacement" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)

docs/API.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
112112
- [`stretchShortVideoTrack`](#stretchshortvideotrack)
113113
- [`maxAudioFramesDrift`](#maxaudioframesdrift)
114114
- [`forceKeyFrameOnDiscontinuity`](#forcekeyframeondiscontinuity)
115+
- [`handleMpegTsVideoIntegrityErrors`](#handlempegtsvideointegrityerrors)
115116
- [`abrEwmaFastLive`](#abrewmafastlive)
116117
- [`abrEwmaSlowLive`](#abrewmaslowlive)
117118
- [`abrEwmaFastVoD`](#abrewmafastvod)
@@ -472,6 +473,7 @@ var config = {
472473
stretchShortVideoTrack: false,
473474
maxAudioFramesDrift: 1,
474475
forceKeyFrameOnDiscontinuity: true,
476+
handleMpegTsVideoIntegrityErrors: 'process',
475477
abrEwmaFastLive: 3.0,
476478
abrEwmaSlowLive: 9.0,
477479
abrEwmaFastVoD: 3.0,
@@ -1489,6 +1491,17 @@ Setting this parameter to false can also generate decoding weirdness when switch
14891491

14901492
parameter should be a boolean
14911493

1494+
### `handleMpegTsVideoIntegrityErrors`
1495+
1496+
(default: `'process'`)
1497+
1498+
Controls how corrupted video data is handled based on MPEG-TS integrity checks.
1499+
1500+
- `'process'` (default): Continues processing corrupted data, which may lead to decoding errors.
1501+
- `'skip'`: Discards corrupted video data to prevent potential playback issues.
1502+
1503+
This parameter accepts a string with possible values: `'process'` | `'skip'`.
1504+
14921505
### `abrEwmaFastLive`
14931506

14941507
(default: `3.0`)

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export type TimelineControllerConfig = {
272272

273273
export type TSDemuxerConfig = {
274274
forceKeyFrameOnDiscontinuity: boolean;
275+
handleMpegTsVideoIntegrityErrors: 'process' | 'skip';
275276
};
276277

277278
export type HlsConfig = {
@@ -421,6 +422,7 @@ export const hlsDefaultConfig: HlsConfig = {
421422
stretchShortVideoTrack: false, // used by mp4-remuxer
422423
maxAudioFramesDrift: 1, // used by mp4-remuxer
423424
forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer
425+
handleMpegTsVideoIntegrityErrors: 'process', // used by ts-demuxer
424426
abrEwmaFastLive: 3, // used by abr-controller
425427
abrEwmaSlowLive: 9, // used by abr-controller
426428
abrEwmaFastVoD: 3, // used by abr-controller

src/demux/tsdemuxer.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class TSDemuxer implements Demuxer {
7272
private aacOverFlow: AudioFrame | null = null;
7373
private remainderData: Uint8Array | null = null;
7474
private videoParser: BaseVideoParser | null;
75+
private videoIntegrityChecker: PacketsIntegrityChecker | null = null;
7576

7677
constructor(
7778
observer: HlsEventEmitter,
@@ -182,6 +183,10 @@ class TSDemuxer implements Demuxer {
182183

183184
this._videoTrack = TSDemuxer.createTrack('video') as DemuxedVideoTrack;
184185
this._videoTrack.duration = trackDuration;
186+
this.videoIntegrityChecker =
187+
this.config.handleMpegTsVideoIntegrityErrors === 'skip'
188+
? new PacketsIntegrityChecker(this.logger)
189+
: null;
185190
this._audioTrack = TSDemuxer.createTrack(
186191
'audio',
187192
trackDuration,
@@ -228,6 +233,7 @@ class TSDemuxer implements Demuxer {
228233
let pes: PES | null;
229234

230235
const videoTrack = this._videoTrack as DemuxedVideoTrack;
236+
const videoIntegrityChecker = this.videoIntegrityChecker;
231237
const audioTrack = this._audioTrack as DemuxedAudioTrack;
232238
const id3Track = this._id3Track as DemuxedMetadataTrack;
233239
const textTrack = this._txtTrack as DemuxedUserdataTrack;
@@ -291,15 +297,21 @@ class TSDemuxer implements Demuxer {
291297
switch (pid) {
292298
case videoPid:
293299
if (stt) {
294-
if (videoData && (pes = parsePES(videoData, this.logger))) {
300+
if (
301+
videoData &&
302+
!videoIntegrityChecker?.isCorrupted &&
303+
(pes = parsePES(videoData, this.logger))
304+
) {
295305
this.readyVideoParser(videoTrack.segmentCodec);
296306
if (this.videoParser !== null) {
297307
this.videoParser.parsePES(videoTrack, textTrack, pes, false);
298308
}
299309
}
300310

301311
videoData = { data: [], size: 0 };
312+
videoIntegrityChecker?.reset(videoPid);
302313
}
314+
videoIntegrityChecker?.handlePacket(data.subarray(start));
303315
if (videoData) {
304316
videoData.data.push(data.subarray(offset, start + PACKET_LENGTH));
305317
videoData.size += start + PACKET_LENGTH - offset;
@@ -464,9 +476,14 @@ class TSDemuxer implements Demuxer {
464476
const videoData = videoTrack.pesData;
465477
const audioData = audioTrack.pesData;
466478
const id3Data = id3Track.pesData;
479+
const videoIntegrityChecker = this.videoIntegrityChecker;
467480
// try to parse last PES packets
468481
let pes: PES | null;
469-
if (videoData && (pes = parsePES(videoData, this.logger))) {
482+
if (
483+
videoData &&
484+
!videoIntegrityChecker?.isCorrupted &&
485+
(pes = parsePES(videoData, this.logger))
486+
) {
470487
this.readyVideoParser(videoTrack.segmentCodec);
471488
if (this.videoParser !== null) {
472489
this.videoParser.parsePES(
@@ -590,6 +607,8 @@ class TSDemuxer implements Demuxer {
590607
this._id3Track =
591608
this._txtTrack =
592609
undefined;
610+
611+
this.videoIntegrityChecker = null;
593612
}
594613

595614
private parseAACPES(track: DemuxedAudioTrack, pes: PES) {
@@ -1050,4 +1069,77 @@ function parsePES(stream: ElementaryStreamData, logger: ILogger): PES | null {
10501069
return null;
10511070
}
10521071

1072+
// See FFMpeg for reference: https://github.com/FFmpeg/FFmpeg/blob/e4c8e80a2efee275f2a10fcf0424c9fc1d86e309/libavformat/mpegts.c#L2811-L2834
1073+
class PacketsIntegrityChecker {
1074+
private readonly logger: ILogger;
1075+
1076+
private pid: number = 0;
1077+
private lastContinuityCounter = -1;
1078+
private integrityState: 'ok' | 'tei-bit' | 'cc-failed' = 'ok';
1079+
1080+
constructor(logger: ILogger) {
1081+
this.logger = logger;
1082+
}
1083+
1084+
public get isCorrupted(): boolean {
1085+
return this.integrityState !== 'ok';
1086+
}
1087+
1088+
public reset(pid: number) {
1089+
this.pid = pid;
1090+
this.lastContinuityCounter = -1;
1091+
this.integrityState = 'ok';
1092+
}
1093+
1094+
public handlePacket(data: Uint8Array) {
1095+
if (data.byteLength < 4) {
1096+
return;
1097+
}
1098+
1099+
const pid = parsePID(data, 0);
1100+
if (pid !== this.pid) {
1101+
this.logger.debug(`Packet PID mismatch, expected ${this.pid} got ${pid}`);
1102+
return;
1103+
}
1104+
1105+
const adaptationFieldControl = (data[3] & 0x30) >> 4;
1106+
if (adaptationFieldControl === 0) {
1107+
return;
1108+
}
1109+
const continuityCounter = data[3] & 0xf;
1110+
1111+
const lastContinuityCounter = this.lastContinuityCounter;
1112+
this.lastContinuityCounter = continuityCounter;
1113+
1114+
const hasPayload = (adaptationFieldControl & 0b01) != 0;
1115+
const hasAdaptation = (adaptationFieldControl & 0b10) != 0;
1116+
const isDiscontinuity =
1117+
hasAdaptation && data[4] != 0 && (data[5] & 0x80) != 0;
1118+
1119+
if (isDiscontinuity) {
1120+
return;
1121+
}
1122+
if (lastContinuityCounter < 0) {
1123+
return;
1124+
}
1125+
1126+
const expectedContinuityCounter = hasPayload
1127+
? (lastContinuityCounter + 1) & 0x0f
1128+
: lastContinuityCounter;
1129+
if (continuityCounter !== expectedContinuityCounter) {
1130+
this.logger.warn(
1131+
`MPEG-TS Continuity Counter check failed for PID='${pid}', CC=${continuityCounter}, Expected-CC=${expectedContinuityCounter} Last-CC=${lastContinuityCounter}`,
1132+
);
1133+
this.integrityState = 'cc-failed';
1134+
return;
1135+
}
1136+
1137+
if ((data[1] & 0x80) !== 0) {
1138+
this.logger.warn(`MPEG-TS Packet had TEI flag set for PID='${pid}'`);
1139+
this.integrityState = 'tei-bit';
1140+
return;
1141+
}
1142+
}
1143+
}
1144+
10531145
export default TSDemuxer;

0 commit comments

Comments
 (0)