Skip to content

Commit

Permalink
Do not append 608/WebVTT/IMSC cues that have alreading been appended
Browse files Browse the repository at this point in the history
Merges changes from #3321 into master
  • Loading branch information
Rob Walch committed Jan 25, 2021
1 parent cc499ec commit c24c554
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 64 deletions.
108 changes: 58 additions & 50 deletions src/controller/timeline-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class TimelineController implements ComponentAPI {
hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this);
}

addCues(
public addCues(
trackName: string,
startTime: number,
endTime: number,
Expand Down Expand Up @@ -158,14 +158,11 @@ export class TimelineController implements ComponentAPI {
}

if (this.config.renderTextTracksNatively) {
this.Cues.newCue(
this.captionsTracks[trackName],
startTime,
endTime,
screen
);
const track = this.captionsTracks[trackName];
const cues = this.Cues.newCue(startTime, endTime, screen);
cues.forEach((cue) => this.addCueToTrack(track, cue));
} else {
const cues = this.Cues.newCue(null, startTime, endTime, screen);
const cues = this.Cues.newCue(startTime, endTime, screen);
this.hls.trigger(Events.CUES_PARSED, {
type: 'captions',
cues,
Expand All @@ -175,7 +172,7 @@ export class TimelineController implements ComponentAPI {
}

// Triggered when an initial PTS is found; used for synchronisation of WebVTT.
onInitPtsFound(
private onInitPtsFound(
event: Events.INIT_PTS_FOUND,
{ frag, id, initPTS, timescale }: InitPTSFoundData
) {
Expand All @@ -195,7 +192,7 @@ export class TimelineController implements ComponentAPI {
}
}

getExistingTrack(trackName: string): TextTrack | null {
private getExistingTrack(trackName: string): TextTrack | null {
const { media } = this;
if (media) {
for (let i = 0; i < media.textTracks.length; i++) {
Expand All @@ -208,15 +205,15 @@ export class TimelineController implements ComponentAPI {
return null;
}

createCaptionsTrack(trackName: string) {
public createCaptionsTrack(trackName: string) {
if (this.config.renderTextTracksNatively) {
this.createNativeTrack(trackName);
} else {
this.createNonNativeTrack(trackName);
}
}

createNativeTrack(trackName: string) {
private createNativeTrack(trackName: string) {
if (this.captionsTracks[trackName]) {
return;
}
Expand All @@ -238,7 +235,7 @@ export class TimelineController implements ComponentAPI {
}
}

createNonNativeTrack(trackName: string) {
private createNonNativeTrack(trackName: string) {
if (this.nonNativeCaptionsTracks[trackName]) {
return;
}
Expand All @@ -259,7 +256,7 @@ export class TimelineController implements ComponentAPI {
this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { tracks: [track] });
}

createTextTrack(
private createTextTrack(
kind: TextTrackKind,
label: string,
lang?: string
Expand All @@ -271,16 +268,19 @@ export class TimelineController implements ComponentAPI {
return media.addTextTrack(kind, label, lang);
}

destroy() {
public destroy() {
this._unregisterListeners();
}

onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
private onMediaAttaching(
event: Events.MEDIA_ATTACHING,
data: MediaAttachingData
) {
this.media = data.media;
this._cleanTracks();
}

onMediaDetaching() {
private onMediaDetaching() {
const { captionsTracks } = this;
Object.keys(captionsTracks).forEach((trackName) => {
clearCurrentCues(captionsTracks[trackName]);
Expand All @@ -289,7 +289,7 @@ export class TimelineController implements ComponentAPI {
this.nonNativeCaptionsTracks = {};
}

onManifestLoading() {
private onManifestLoading() {
this.lastSn = -1; // Detect discontinuity in fragment parsing
this.prevCC = -1;
this.vttCCs = newVTTCCs(); // Detect discontinuity in subtitle manifests
Expand All @@ -307,7 +307,7 @@ export class TimelineController implements ComponentAPI {
}
}

_cleanTracks() {
private _cleanTracks() {
// clear outdated subtitles
const { media } = this;
if (!media) {
Expand All @@ -321,7 +321,7 @@ export class TimelineController implements ComponentAPI {
}
}

onSubtitleTracksUpdated(
private onSubtitleTracksUpdated(
event: Events.SUBTITLE_TRACKS_UPDATED,
data: SubtitleTracksUpdatedData
) {
Expand Down Expand Up @@ -385,7 +385,10 @@ export class TimelineController implements ComponentAPI {
}
}

onManifestLoaded(event: Events.MANIFEST_LOADED, data: ManifestLoadedData) {
private onManifestLoaded(
event: Events.MANIFEST_LOADED,
data: ManifestLoadedData
) {
if (this.config.enableCEA708Captions && data.captions) {
data.captions.forEach((captionsTrack) => {
const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(
Expand All @@ -411,7 +414,7 @@ export class TimelineController implements ComponentAPI {
}
}

onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
const { frag, payload } = data;
const {
cea608Parser1,
Expand Down Expand Up @@ -484,6 +487,29 @@ export class TimelineController implements ComponentAPI {
}
}

private addCueToTrack(track: TextTrack, cue: VTTCue) {
// Sometimes there are cue overlaps on segmented vtts so the same
// cue can appear more than once in different vtt files.
// This avoid showing duplicated cues with same timecode and text.
if (!track.cues!.getCueById(cue.id)) {
try {
track.addCue(cue);
if (!track.cues!.getCueById(cue.id)) {
throw new Error(`addCue is failed for: ${cue}`);
}
} catch (err) {
logger.debug(`Failed occurred on adding cues: ${err}`);
const textTrackCue = new (self.TextTrackCue as any)(
cue.startTime,
cue.endTime,
cue.text
);
textTrackCue.id = cue.id;
track.addCue(textTrackCue);
}
}
}

private _parseIMSC1(frag: Fragment, payload: ArrayBuffer) {
const hls = this.hls;
parseIMSC1(
Expand Down Expand Up @@ -557,47 +583,29 @@ export class TimelineController implements ComponentAPI {
}
}

private _appendCues(cues, fragLevel) {
private _appendCues(cues: VTTCue[], fragLevel: number) {
const hls = this.hls;
if (this.config.renderTextTracksNatively) {
const textTrack = this.textTracks[fragLevel];
// WebVTTParser.parse is an async method and if the currently selected text track mode is set to "disabled"
// before parsing is done then don't try to access currentTrack.cues.getCueById as cues will be null
// and trying to access getCueById method of cues will throw an exception
// Because we check if the mode is diabled, we can force check `cues` below. They can't be null.
// Because we check if the mode is disabled, we can force check `cues` below. They can't be null.
if (textTrack.mode === 'disabled') {
return;
}
// Sometimes there are cue overlaps on segmented vtts so the same
// cue can appear more than once in different vtt files.
// This avoid showing duplicated cues with same timecode and text.
cues
.filter((cue) => !textTrack.cues!.getCueById(cue.id))
.forEach((cue) => {
try {
textTrack.addCue(cue);
if (!textTrack.cues!.getCueById(cue.id)) {
throw new Error(`addCue is failed for: ${cue}`);
}
} catch (err) {
logger.debug(`Failed occurred on adding cues: ${err}`);
const textTrackCue = new (self.TextTrackCue as any)(
cue.startTime,
cue.endTime,
cue.text
);
textTrackCue.id = cue.id;
textTrack.addCue(textTrackCue);
}
});
cues.forEach((cue) => this.addCueToTrack(textTrack, cue));
} else {
const currentTrack = this.tracks[fragLevel];
const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel;
hls.trigger(Events.CUES_PARSED, { type: 'subtitles', cues, track });
}
}

onFragDecrypted(event: Events.FRAG_DECRYPTED, data: FragDecryptedData) {
private onFragDecrypted(
event: Events.FRAG_DECRYPTED,
data: FragDecryptedData
) {
const { frag } = data;
if (frag.type === PlaylistLevelType.SUBTITLE) {
if (!Number.isFinite(this.initPTS[frag.cc])) {
Expand All @@ -611,12 +619,12 @@ export class TimelineController implements ComponentAPI {
}
}

onSubtitleTracksCleared() {
private onSubtitleTracksCleared() {
this.tracks = [];
this.captionsTracks = {};
}

onFragParsingUserdata(
private onFragParsingUserdata(
event: Events.FRAG_PARSING_USERDATA,
data: FragParsingUserdataData
) {
Expand All @@ -637,7 +645,7 @@ export class TimelineController implements ComponentAPI {
}
}

extractCea608Data(byteArray: Uint8Array): number[][] {
private extractCea608Data(byteArray: Uint8Array): number[][] {
const count = byteArray[0] & 31;
let position = 2;
const actualCCBytes: number[][] = [[], []];
Expand Down
11 changes: 4 additions & 7 deletions src/utils/cues.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { fixLineBreaks } from './vttparser';
import type { CaptionScreen, Row } from './cea-608-parser';
import { generateCueId } from './webvtt-parser';

const WHITESPACE_CHAR = /\s/;

export interface CuesInterface {
newCue(
track: TextTrack | null,
startTime: number,
endTime: number,
captionScreen: CaptionScreen
): VTTCue[];
}

export function newCue(
track: TextTrack | null,
startTime: number,
endTime: number,
captionScreen: CaptionScreen
Expand Down Expand Up @@ -51,6 +50,7 @@ export function newCue(
}

cue = new Cue(startTime, endTime, fixLineBreaks(text.trim()));
cue.id = generateCueId(cue.startTime, cue.endTime, cue.text);

if (indent >= 16) {
indent--;
Expand All @@ -67,9 +67,9 @@ export function newCue(
result.push(cue);
}
}
if (track && result.length) {
if (result.length) {
// Sort bottom cues in reverse order so that they render in line order when overlapping in Chrome
const sortedCues = result.sort((cueA, cueB) => {
return result.sort((cueA, cueB) => {
if (cueA.line === 'auto' || cueB.line === 'auto') {
return 0;
}
Expand All @@ -78,9 +78,6 @@ export function newCue(
}
return cueA.line - cueB.line;
});
for (let i = 0; i < sortedCues.length; i++) {
track.addCue(sortedCues[i]);
}
}
return result;
}
19 changes: 12 additions & 7 deletions src/utils/webvtt-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ const hash = function (text: string) {
return (hash >>> 0).toString();
};

// Create a unique hash id for a cue based on start/end times and text.
// This helps timeline-controller to avoid showing repeated captions.
export function generateCueId(
startTime: number,
endTime: number,
text: string
) {
return hash(startTime.toString()) + hash(endTime.toString()) + hash(text);
}

const calculateOffset = function (vttCCs: VTTCCs, cc, presentationTime) {
let currCC = vttCCs[cc];
let prevCC = vttCCs[currCC.prevCC];
Expand Down Expand Up @@ -135,14 +145,9 @@ export function parseWebVTT(
cue.endTime = startTime + duration;
}

// If the cue was not assigned an id from the VTT file (line above the content),
// then create a unique hash id for a cue based on start/end times.
// This helps timeline-controller to avoid showing repeated captions.
// If the cue was not assigned an id from the VTT file (line above the content), create one.
if (!cue.id) {
cue.id =
hash(cue.startTime.toString()) +
hash(cue.endTime.toString()) +
hash(cue.text);
cue.id = generateCueId(cue.startTime, cue.endTime, cue.text);
}

// Fix encoding of special characters
Expand Down

0 comments on commit c24c554

Please sign in to comment.