Skip to content

Commit

Permalink
Do not clear subtitle cues or fragment tracking when updated subtitle…
Browse files Browse the repository at this point in the history
… tracks are identical (#5365)

Fixes #5361
  • Loading branch information
robwalch committed Apr 5, 2023
1 parent c5134d8 commit 9cafa43
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 25 deletions.
18 changes: 12 additions & 6 deletions src/controller/subtitle-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FragmentState } from './fragment-tracker';
import BaseStreamController, { State } from './base-stream-controller';
import { PlaylistLevelType } from '../types/loader';
import { Level } from '../types/level';
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
import { ErrorDetails, ErrorTypes } from '../errors';
import type { NetworkComponentAPI } from '../types/component-api';
import type Hls from '../hls';
Expand Down Expand Up @@ -221,19 +222,24 @@ export class SubtitleStreamController
event: Events.SUBTITLE_TRACKS_UPDATED,
{ subtitleTracks }: SubtitleTracksUpdatedData
) {
if (subtitleOptionsIdentical(this.levels, subtitleTracks)) {
this.levels = subtitleTracks.map(
(mediaPlaylist) => new Level(mediaPlaylist)
);
return;
}
this.tracksBuffered = [];
this.levels = subtitleTracks.map(
(mediaPlaylist) => new Level(mediaPlaylist)
);
this.levels = subtitleTracks.map((mediaPlaylist) => {
const level = new Level(mediaPlaylist);
this.tracksBuffered[level.id] = [];
return level;
});
this.fragmentTracker.removeFragmentsInRange(
0,
Number.POSITIVE_INFINITY,
PlaylistLevelType.SUBTITLE
);
this.fragPrevious = null;
this.levels.forEach((level: Level) => {
this.tracksBuffered[level.id] = [];
});
this.mediaBuffer = null;
}

Expand Down
27 changes: 17 additions & 10 deletions src/controller/timeline-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
addCueToTrack,
removeCuesInRange,
} from '../utils/texttrack-utils';
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
import { parseIMSC1, IMSC1_CODEC } from '../utils/imsc1-ttml-parser';
import { appendUint8Array } from '../utils/mp4-tools';
import { PlaylistLevelType } from '../types/loader';
Expand Down Expand Up @@ -329,20 +330,23 @@ export class TimelineController implements ComponentAPI {
event: Events.SUBTITLE_TRACKS_UPDATED,
data: SubtitleTracksUpdatedData
) {
this.textTracks = [];
const tracks: Array<MediaPlaylist> = data.subtitleTracks || [];
const hasIMSC1 = tracks.some((track) => track.textCodec === IMSC1_CODEC);
if (this.config.enableWebVTT || (hasIMSC1 && this.config.enableIMSC1)) {
const sameTracks =
this.tracks && tracks && this.tracks.length === tracks.length;
this.tracks = tracks || [];
const listIsIdentical = subtitleOptionsIdentical(this.tracks, tracks);
if (listIsIdentical) {
this.tracks = tracks;
return;
}
this.textTracks = [];
this.tracks = tracks;

if (this.config.renderTextTracksNatively) {
const inUseTracks = this.media ? this.media.textTracks : [];
const inUseTracks = this.media ? this.media.textTracks : null;

this.tracks.forEach((track, index) => {
let textTrack: TextTrack | undefined;
if (index < inUseTracks.length) {
if (inUseTracks && index < inUseTracks.length) {
let inUseTrack: TextTrack | null = null;

for (let i = 0; i < inUseTracks.length; i++) {
Expand Down Expand Up @@ -376,7 +380,7 @@ export class TimelineController implements ComponentAPI {
this.textTracks.push(textTrack);
}
});
} else if (!sameTracks && this.tracks && this.tracks.length) {
} else if (this.tracks.length) {
// Create a list of tracks for the provider to consume
const tracksList = this.tracks.map((track) => {
return {
Expand All @@ -396,7 +400,7 @@ export class TimelineController implements ComponentAPI {
private _captionsOrSubtitlesFromCharacteristics(
track: MediaPlaylist
): TextTrackKind {
if (track.attrs?.CHARACTERISTICS) {
if (track.attrs.CHARACTERISTICS) {
const transcribesSpokenDialog = /transcribes-spoken-dialog/gi.test(
track.attrs.CHARACTERISTICS
);
Expand Down Expand Up @@ -728,9 +732,12 @@ export class TimelineController implements ComponentAPI {
}
}

function canReuseVttTextTrack(inUseTrack, manifestTrack): boolean {
function canReuseVttTextTrack(
inUseTrack: (TextTrack & { textTrack1?; textTrack2? }) | null,
manifestTrack: MediaPlaylist
): boolean {
return (
inUseTrack &&
!!inUseTrack &&
inUseTrack.label === manifestTrack.name &&
!(inUseTrack.textTrack1 || inUseTrack.textTrack2)
);
Expand Down
45 changes: 45 additions & 0 deletions src/utils/media-option-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Level } from '../types/level';
import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';

export function subtitleOptionsIdentical(
trackList1: MediaPlaylist[] | Level[],
trackList2: MediaPlaylist[]
): boolean {
if (trackList1.length !== trackList2.length) {
return false;
}
for (let i = 0; i < trackList1.length; i++) {
if (
!subtitleAttributesIdentical(
trackList1[i].attrs as MediaAttributes,
trackList2[i].attrs
)
) {
return false;
}
}
return true;
}

export function subtitleAttributesIdentical(
attrs1: MediaAttributes,
attrs2: MediaAttributes
): boolean {
// Media options with the same rendition ID must be bit identical
const stableRenditionId = attrs1['STABLE-RENDITION-ID'];
if (stableRenditionId) {
return stableRenditionId === attrs2['STABLE-RENDITION-ID'];
}
// When rendition ID is not present, compare attributes
return ![
'LANGUAGE',
'NAME',
'CHARACTERISTICS',
'AUTOSELECT',
'DEFAULT',
'FORCED',
].some(
(subtitleAttribute) =>
attrs1[subtitleAttribute] !== attrs2[subtitleAttribute]
);
}
13 changes: 12 additions & 1 deletion tests/unit/controller/subtitle-stream-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ const mediaMock = {
removeEventListener() {},
};

const tracksMock = [{ id: 0, details: { url: '', fragments: [] } }, { id: 1 }];
const tracksMock = [
{
id: 0,
details: { url: '', fragments: [] },
attrs: {},
},
{
id: 1,
attrs: {},
},
];

describe('SubtitleStreamController', function () {
let hls;
Expand Down Expand Up @@ -41,6 +51,7 @@ describe('SubtitleStreamController', function () {
subtitleStreamController.onMediaDetaching(Events.MEDIA_DETACHING, {
media: mediaMock,
});
hls.destroy();
});

describe('onSubtitleTracksUpdate', function () {
Expand Down
17 changes: 9 additions & 8 deletions tests/unit/controller/timeline-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('TimelineController', function () {
Events.SUBTITLE_TRACKS_UPDATED,
{
subtitleTracks: [
{ id: 0, name: 'en' },
{ id: 1, name: 'ru' },
{ id: 0, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 1, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
],
}
);
Expand All @@ -41,8 +41,8 @@ describe('TimelineController', function () {
Events.SUBTITLE_TRACKS_UPDATED,
{
subtitleTracks: [
{ id: 0, name: 'en' },
{ id: 1, name: 'ru' },
{ id: 0, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 1, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
],
}
);
Expand All @@ -62,8 +62,8 @@ describe('TimelineController', function () {

timelineController.onSubtitleTracksUpdated(Events.MANIFEST_LOADED, {
subtitleTracks: [
{ id: 0, name: 'en' },
{ id: 1, name: 'ru' },
{ id: 0, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 1, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
],
});

Expand All @@ -78,8 +78,8 @@ describe('TimelineController', function () {

timelineController.onSubtitleTracksUpdated(Events.MANIFEST_LOADED, {
subtitleTracks: [
{ id: 0, name: 'ru' },
{ id: 1, name: 'en' },
{ id: 0, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
{ id: 1, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
],
});

Expand Down Expand Up @@ -146,6 +146,7 @@ describe('TimelineController', function () {
{
id: 0,
name: 'en',
attrs: {},
},
],
});
Expand Down

0 comments on commit 9cafa43

Please sign in to comment.