Skip to content

Commit 8d52455

Browse files
authored
Merge branch 'Dash-Industry-Forum:development' into development
2 parents 1fa0a1d + 68d8f7f commit 8d52455

File tree

7 files changed

+301
-47
lines changed

7 files changed

+301
-47
lines changed

samples/network-interceptor/package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/network-interceptor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"build": "webpack --mode development"
44
},
55
"devDependencies": {
6-
"@svta/common-media-library": "^0.11.0",
6+
"@svta/common-media-library": "^0.12.4",
77
"@types/node": "^18.14.0",
88
"copy-webpack-plugin": "^12.0.2",
99
"dashjs": "file:../..",

src/streaming/text/CueSet.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* The copyright in this software is being made available under the BSD License,
3+
* included below. This software may be subject to other third party and contributor
4+
* rights, including patent rights, and no such rights are granted under this license.
5+
*
6+
* Copyright (c) 2013, Dash Industry Forum.
7+
* All rights reserved.
8+
*
9+
* Redistribution and use in source and binary forms, with or without modification,
10+
* are permitted provided that the following conditions are met:
11+
* * Redistributions of source code must retain the above copyright notice, this
12+
* list of conditions and the following disclaimer.
13+
* * Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation and/or
15+
* other materials provided with the distribution.
16+
* * Neither the name of Dash Industry Forum nor the names of its
17+
* contributors may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
21+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23+
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
24+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
25+
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27+
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
* POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
/**
33+
* @classdesc Similar to Set<TextTrackCue>, but using the {@link areCuesEqual} function to compare cues, instead of ===.
34+
* @ignore
35+
*/
36+
class CueSet {
37+
38+
/**
39+
* The cues contained in the set, grouped by start time.
40+
*
41+
* @instance
42+
* @type {Map<number, TextTrackCue[]>}
43+
* @name CueSet.cues
44+
* @memberof CueSet
45+
*/
46+
47+
/**
48+
* Creates a new CueSet instance.
49+
*
50+
* @param {ArrayLike<TextTrackCue>} [initialCues] - Optional initial cues to add to the set.
51+
*/
52+
constructor(initialCues) {
53+
this.cues = new Map();
54+
if (initialCues) {
55+
for (const cue of initialCues) {
56+
this.addCue(cue);
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Checks if a cue is already in the set.
63+
*
64+
* @param {TextTrackCue} cue
65+
* @returns {boolean}
66+
*/
67+
hasCue(cue) {
68+
const cuesWithSameStartTime = this.cues.get(cue.startTime);
69+
return cuesWithSameStartTime && cuesWithSameStartTime.some(c => areCuesEqual(c, cue));
70+
}
71+
72+
/**
73+
* Adds a cue to the set, if it is not already present.
74+
*
75+
* @param {TextTrackCue} cue
76+
*/
77+
addCue(cue) {
78+
const cuesWithSameStartTime = this.cues.get(cue.startTime);
79+
80+
if (!cuesWithSameStartTime) {
81+
this.cues.set(cue.startTime, [cue]);
82+
} else if (!this.hasCue(cue)) {
83+
cuesWithSameStartTime.push(cue);
84+
}
85+
}
86+
}
87+
88+
/**
89+
* Compares two cues for equality.
90+
*
91+
* @param {TextTrackCue} cue1
92+
* @param {TextTrackCue} cue2
93+
* @returns {boolean}
94+
* @private
95+
*/
96+
function areCuesEqual(cue1, cue2) {
97+
if (cue1.startTime !== cue2.startTime ||
98+
cue1.endTime !== cue2.endTime) {
99+
return false;
100+
}
101+
if (cue1 instanceof VTTCue && cue2 instanceof VTTCue) {
102+
return cue1.text === cue2.text;
103+
}
104+
return false;
105+
}
106+
107+
export { CueSet };

src/streaming/text/TextTracks.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import Events from '../../core/events/Events.js';
3434
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents.js';
3535
import FactoryMaker from '../../core/FactoryMaker.js';
3636
import Debug from '../../core/Debug.js';
37+
import Utils from '../../core/Utils.js';
38+
import {CueSet} from './CueSet.js';
3739
import {renderHTML} from 'imsc';
3840

3941
const CUE_PROPS_TO_COMPARE = [
@@ -496,6 +498,8 @@ function TextTracks(config) {
496498
return;
497499
}
498500

501+
const cueSet = new CueSet(track.cues);
502+
499503
for (let item = 0; item < captionData.length; item++) {
500504
let cue = null;
501505
const currentItem = captionData[item];
@@ -515,7 +519,8 @@ function TextTracks(config) {
515519

516520
try {
517521
if (cue) {
518-
if (!cueInTrack(track, cue)) {
522+
if (!cueSet.hasCue(cue)) {
523+
cueSet.addCue(cue);
519524
if (settings.get().streaming.text.webvtt.customRenderingEnabled) {
520525
if (!track.manualCueList) {
521526
track.manualCueList = [];
@@ -661,12 +666,18 @@ function TextTracks(config) {
661666
if (currentItem.styles.line !== undefined && 'line' in cue) {
662667
cue.line = currentItem.styles.line;
663668
}
669+
if (currentItem.styles.lineAlign !== undefined) {
670+
cue.lineAlign = currentItem.styles.lineAlign;
671+
}
664672
if (currentItem.styles.snapToLines !== undefined && 'snapToLines' in cue) {
665673
cue.snapToLines = currentItem.styles.snapToLines;
666674
}
667675
if (currentItem.styles.position !== undefined && 'position' in cue) {
668676
cue.position = currentItem.styles.position;
669677
}
678+
if (currentItem.styles.positionAlign !== undefined) {
679+
cue.positionAlign = currentItem.styles.positionAlign;
680+
}
670681
if (currentItem.styles.size !== undefined && 'size' in cue) {
671682
cue.size = currentItem.styles.size;
672683
}
@@ -720,7 +731,7 @@ function TextTracks(config) {
720731

721732
function _getCueInformationForNonHtml(currentItem, timeOffset) {
722733
let cue = new Cue(currentItem.start - timeOffset, currentItem.end - timeOffset, currentItem.data);
723-
cue.cueID = `${cue.startTime}_${cue.endTime}`;
734+
cue.cueID = Utils.generateUuid();
724735
return cue;
725736
}
726737

@@ -868,19 +879,6 @@ function TextTracks(config) {
868879
}
869880
}
870881

871-
function cueInTrack(track, cue) {
872-
if (!track.cues) {
873-
return false;
874-
}
875-
for (let i = 0; i < track.cues.length; i++) {
876-
if ((track.cues[i].startTime === cue.startTime) &&
877-
(track.cues[i].endTime === cue.endTime)) {
878-
return true;
879-
}
880-
}
881-
return false;
882-
}
883-
884882
function cueInRange(cue, start, end, strict = true) {
885883
if (!cue) {
886884
return false

src/streaming/utils/VTTParser.js

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -124,27 +124,41 @@ function VTTParser() {
124124
function getCaptionStyles(arr) {
125125
const styleObject = {};
126126
arr.forEach(function (element) {
127-
if (element.split(/:/).length > 1) {
128-
let val = element.split(/:/)[1];
129-
let isPercentage = false;
130-
if (val && val.search(/%/) != -1) {
131-
isPercentage = true;
132-
val = parseInt(val.replace(/%/, ''), 10);
133-
}
134-
if (element.match(/align/) || element.match(/A/)) {
135-
styleObject.align = val;
136-
}
137-
if (element.match(/line/) || element.match(/L/)) {
138-
styleObject.line = val === 'auto' ? val : parseInt(val, 10);
139-
if (isPercentage) {
140-
styleObject.snapToLines = false;
141-
}
142-
}
143-
if (element.match(/position/) || element.match(/P/)) {
144-
styleObject.position = val;
145-
}
146-
if (element.match(/size/) || element.match(/S/)) {
147-
styleObject.size = val;
127+
const parts = element.split(':');
128+
129+
if (parts.length > 1) {
130+
const [settingName, settingValue] = parts;
131+
132+
switch (settingName) {
133+
case 'align':
134+
case 'A':
135+
styleObject.align = settingValue;
136+
break;
137+
case 'line':
138+
case 'L':
139+
const [line, lineAlign] = settingValue.split(',');
140+
const isPercentage = line.endsWith('%');
141+
142+
styleObject.line = line === 'auto' ? line : parseInt(line, 10);
143+
if (isPercentage) {
144+
styleObject.snapToLines = false;
145+
}
146+
if (lineAlign) {
147+
styleObject.lineAlign = lineAlign;
148+
}
149+
break;
150+
case 'position':
151+
case 'P':
152+
const [position, positionAlign] = settingValue.split(',');
153+
styleObject.position = parseInt(position, 10);
154+
if (positionAlign) {
155+
styleObject.positionAlign = positionAlign;
156+
}
157+
break;
158+
case 'size':
159+
case 'S':
160+
styleObject.size = settingValue;
161+
break;
148162
}
149163
}
150164
});

test/unit/test/streaming/streaming.text.TextTracks.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,55 @@ describe('TextTracks', function () {
9090

9191
expect(videoModelMock.getCurrentCue(track).text).to.equal(SUBTITLE_DATA);
9292
});
93+
94+
it('should eliminate duplicates', function () {
95+
textTracks.addTextTrackInfo({
96+
index: 0,
97+
kind: 'subtitles',
98+
id: 'eng',
99+
defaultTrack: true,
100+
isTTML: true}, 1);
101+
102+
textTracks.createTracks();
103+
let track = videoModelMock.getTextTrack('subtitles', 'eng');
104+
105+
textTracks.addCaptions(0, 0, [
106+
{type: 'noHtml', data: 'unique cue', start: 0, end: 2},
107+
{type: 'noHtml', data: 'duplicated cue', start: 2, end: 4},
108+
{type: 'noHtml', data: 'duplicated cue', start: 2, end: 4},
109+
]);
110+
111+
textTracks.addCaptions(0, 0, [
112+
{type: 'noHtml', data: 'duplicated cue', start: 2, end: 4},
113+
{type: 'noHtml', data: 'another unique cue', start: 4, end: 6},
114+
]);
115+
116+
expect(track.cues.length).to.equal(3);
117+
});
118+
119+
it('should support multiple cues with same timing, but different text', function () {
120+
textTracks.addTextTrackInfo({
121+
index: 0,
122+
kind: 'subtitles',
123+
id: 'eng',
124+
defaultTrack: true,
125+
isTTML: true}, 1);
126+
127+
textTracks.createTracks();
128+
let track = videoModelMock.getTextTrack('subtitles', 'eng');
129+
130+
const cues = [
131+
{type: 'noHtml', data: 'First cue', start: 0, end: 2},
132+
{type: 'noHtml', data: 'Second cue', start: 0, end: 2}
133+
];
134+
135+
textTracks.addCaptions(0, 0, cues);
136+
137+
const allCues = track.cues
138+
expect(allCues.length).to.equal(2);
139+
expect(allCues[0].text).to.equal('First cue');
140+
expect(allCues[1].text).to.equal('Second cue');
141+
expect(allCues[0].cueID).to.not.equal(allCues[1].cueID);
142+
});
93143
});
94144
});

0 commit comments

Comments
 (0)