Skip to content

Commit e0ddf41

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

File tree

4 files changed

+104
-1
lines changed

4 files changed

+104
-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: 88 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,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,
@@ -227,6 +232,7 @@ class TSDemuxer implements Demuxer {
227232
let pes: PES | null;
228233

229234
const videoTrack = this._videoTrack as DemuxedVideoTrack;
235+
const videoIntegrityChecker = this.videoIntegrityChecker;
230236
const audioTrack = this._audioTrack as DemuxedAudioTrack;
231237
const id3Track = this._id3Track as DemuxedMetadataTrack;
232238
const textTrack = this._txtTrack as DemuxedUserdataTrack;
@@ -290,7 +296,11 @@ class TSDemuxer implements Demuxer {
290296
switch (pid) {
291297
case videoPid:
292298
if (stt) {
293-
if (videoData && (pes = parsePES(videoData, this.logger))) {
299+
if (
300+
videoData &&
301+
!videoIntegrityChecker?.isCorrupted &&
302+
(pes = parsePES(videoData, this.logger))
303+
) {
294304
if (this.videoParser === null) {
295305
switch (videoTrack.segmentCodec) {
296306
case 'avc':
@@ -309,7 +319,9 @@ class TSDemuxer implements Demuxer {
309319
}
310320

311321
videoData = { data: [], size: 0 };
322+
videoIntegrityChecker?.reset(videoPid);
312323
}
324+
videoIntegrityChecker?.handle_packet(data.subarray(start));
313325
if (videoData) {
314326
videoData.data.push(data.subarray(offset, start + PACKET_LENGTH));
315327
videoData.size += start + PACKET_LENGTH - offset;
@@ -597,6 +609,8 @@ class TSDemuxer implements Demuxer {
597609
this._id3Track =
598610
this._txtTrack =
599611
undefined;
612+
613+
this.videoIntegrityChecker = null;
600614
}
601615

602616
private parseAACPES(track: DemuxedAudioTrack, pes: PES) {
@@ -1057,4 +1071,77 @@ function parsePES(stream: ElementaryStreamData, logger: ILogger): PES | null {
10571071
return null;
10581072
}
10591073

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

0 commit comments

Comments
 (0)