Skip to content

Commit 6ea6edb

Browse files
committed
#7064: Check integrity of MPEG-TS video stream.
1 parent 03f96e5 commit 6ea6edb

File tree

4 files changed

+103
-1
lines changed

4 files changed

+103
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4778,6 +4778,7 @@ export interface TransmuxerResult {
47784778
// @public (undocumented)
47794779
export type TSDemuxerConfig = {
47804780
forceKeyFrameOnDiscontinuity: boolean;
4781+
handleMpegTsVideoIntegrityErrors: 'process' | 'skip';
47814782
};
47824783

47834784
// 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
@@ -110,6 +110,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
110110
- [`stretchShortVideoTrack`](#stretchshortvideotrack)
111111
- [`maxAudioFramesDrift`](#maxaudioframesdrift)
112112
- [`forceKeyFrameOnDiscontinuity`](#forcekeyframeondiscontinuity)
113+
- [`handleMpegTsVideoIntegrityErrors`](#handlempegtsvideointegrityerrors)
113114
- [`abrEwmaFastLive`](#abrewmafastlive)
114115
- [`abrEwmaSlowLive`](#abrewmaslowlive)
115116
- [`abrEwmaFastVoD`](#abrewmafastvod)
@@ -470,6 +471,7 @@ var config = {
470471
stretchShortVideoTrack: false,
471472
maxAudioFramesDrift: 1,
472473
forceKeyFrameOnDiscontinuity: true,
474+
handleMpegTsVideoIntegrityErrors: 'process',
473475
abrEwmaFastLive: 3.0,
474476
abrEwmaSlowLive: 9.0,
475477
abrEwmaFastVoD: 3.0,
@@ -1471,6 +1473,17 @@ Setting this parameter to false can also generate decoding weirdness when switch
14711473

14721474
parameter should be a boolean
14731475

1476+
### `handleMpegTsVideoIntegrityErrors`
1477+
1478+
(default: `'process'`)
1479+
1480+
Controls how corrupted video data is handled based on MPEG-TS integrity checks.
1481+
1482+
- `'process'` (default): Continues processing corrupted data, which may lead to decoding errors.
1483+
- `'skip'`: Discards corrupted video data to prevent potential playback issues.
1484+
1485+
This parameter accepts a string with possible values: `'process'` | `'skip'`.
1486+
14741487
### `abrEwmaFastLive`
14751488

14761489
(default: `3.0`)

src/config.ts

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

270270
export type TSDemuxerConfig = {
271271
forceKeyFrameOnDiscontinuity: boolean;
272+
handleMpegTsVideoIntegrityErrors: 'process' | 'skip';
272273
};
273274

274275
export type HlsConfig = {
@@ -413,6 +414,7 @@ export const hlsDefaultConfig: HlsConfig = {
413414
stretchShortVideoTrack: false, // used by mp4-remuxer
414415
maxAudioFramesDrift: 1, // used by mp4-remuxer
415416
forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer
417+
handleMpegTsVideoIntegrityErrors: 'process', // used by ts-demuxer
416418
abrEwmaFastLive: 3, // used by abr-controller
417419
abrEwmaSlowLive: 9, // used by abr-controller
418420
abrEwmaFastVoD: 3, // used by abr-controller

src/demux/tsdemuxer.ts

Lines changed: 87 additions & 1 deletion
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,7 @@ class TSDemuxer implements Demuxer {
182183

183184
this._videoTrack = TSDemuxer.createTrack('video') as DemuxedVideoTrack;
184185
this._videoTrack.duration = trackDuration;
186+
this.videoIntegrityChecker = new PacketsIntegrityChecker(this.logger);
185187
this._audioTrack = TSDemuxer.createTrack(
186188
'audio',
187189
trackDuration,
@@ -227,6 +229,8 @@ class TSDemuxer implements Demuxer {
227229
let pes: PES | null;
228230

229231
const videoTrack = this._videoTrack as DemuxedVideoTrack;
232+
const videoIntegrityChecker = this
233+
.videoIntegrityChecker as PacketsIntegrityChecker;
230234
const audioTrack = this._audioTrack as DemuxedAudioTrack;
231235
const id3Track = this._id3Track as DemuxedMetadataTrack;
232236
const textTrack = this._txtTrack as DemuxedUserdataTrack;
@@ -290,7 +294,12 @@ class TSDemuxer implements Demuxer {
290294
switch (pid) {
291295
case videoPid:
292296
if (stt) {
293-
if (videoData && (pes = parsePES(videoData, this.logger))) {
297+
if (
298+
videoData &&
299+
(!videoIntegrityChecker.isCorrupted ||
300+
this.config.handleMpegTsVideoIntegrityErrors === 'process') &&
301+
(pes = parsePES(videoData, this.logger))
302+
) {
294303
if (this.videoParser === null) {
295304
switch (videoTrack.segmentCodec) {
296305
case 'avc':
@@ -309,7 +318,9 @@ class TSDemuxer implements Demuxer {
309318
}
310319

311320
videoData = { data: [], size: 0 };
321+
videoIntegrityChecker.reset(videoPid);
312322
}
323+
videoIntegrityChecker.handle_packet(data.subarray(start));
313324
if (videoData) {
314325
videoData.data.push(data.subarray(offset, start + PACKET_LENGTH));
315326
videoData.size += start + PACKET_LENGTH - offset;
@@ -597,6 +608,8 @@ class TSDemuxer implements Demuxer {
597608
this._id3Track =
598609
this._txtTrack =
599610
undefined;
611+
612+
this.videoIntegrityChecker = null;
600613
}
601614

602615
private parseAACPES(track: DemuxedAudioTrack, pes: PES) {
@@ -1057,4 +1070,77 @@ function parsePES(stream: ElementaryStreamData, logger: ILogger): PES | null {
10571070
return null;
10581071
}
10591072

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

0 commit comments

Comments
 (0)