Skip to content

Commit

Permalink
fix: Refactor Subtitle DeltaTimes Calculation (#122)
Browse files Browse the repository at this point in the history
* add: independant subtitle deltatimes

* test: update for independant subtitle deltatimes
  • Loading branch information
Nfrederiksen authored Nov 21, 2023
1 parent 2a41fa6 commit fd9cabf
Show file tree
Hide file tree
Showing 8 changed files with 1,081 additions and 23 deletions.
95 changes: 79 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const m3u8 = require("@eyevinn/m3u8");
const debug = require("debug")("hls-vodtolive");
const verbose = require("debug")("hls-vodtolive-verbose");
const { findIndexReversed, fetchWithRetry, urlResolve, segToM3u8, findBottomSegItem } = require("./utils.js");
const { findIndexReversed, fetchWithRetry, urlResolve, segToM3u8, findBottomSegItem, fixedNumber, inspectForVodTransition } = require("./utils.js");

class HLSVod {
/**
Expand Down Expand Up @@ -1152,15 +1152,61 @@ class HLSVod {
leftover = {};
}
const bandwidths = Object.keys(this.segments);
let videoSegments = this.segments[bandwidths[0]]
let segmentList = this.segments[bandwidths[0]];
const subsGroupIdCount = Object.keys(this.subtitleSegments).length;

if (useDummyUrl && subsGroupIdCount > 1) {
let subTrax = [];
for (let group of Object.keys(this.subtitleSegments)) {
for (let lang of Object.keys(this.subtitleSegments[group])) {
subTrax.push({ trackId: `${group}:::${lang}`, seglist: this.subtitleSegments[group][lang] });
}
}
subTrax = subTrax.reverse();
const findItemWithLargestSize = (list) => {
return list.reduce((largestItem, currentItem) => {
return currentItem.seglist.length > largestItem.seglist.length ? currentItem : largestItem;
}).trackId;
};
const track = findItemWithLargestSize(subTrax);
const [g , l] = track.split(":::");
const subsegs = this.subtitleSegments[g][l];
const trackHasDummySpliceSegments = (arr) => {
for (let i = arr.length - 1; i >= 0; i--) {
const seg = arr[i];
if (seg.uri) {
const p = /starttime=.*endtime=.*elapsedtime=/;
return p.test(seg.uri);
};
}
}
// Only generate smaller subtitle segments based on loaded subtitle segments, rather than video segments,
// if loaded subtitle segments exist (and are not 'splice' segments)
if (subsegs.length > 0 && !trackHasDummySpliceSegments(subsegs)) {
const [numSegsAfterVT, hasVT] = inspectForVodTransition(subsegs);
if (numSegsAfterVT > 0 || !hasVT) {
segmentList = this.subtitleSegments[g][l];
let dur = 0;
segmentList.map(s => {
if (s.duration) {
dur += s.duration;
}
});
segment.duration = dur;
}
}

}
let newSegmentList = [];
let totalSubtitleSegmentDuration = segment.duration;
let index = offset;
let allVideoDurationUsed = false;
while (index < videoSegments.length && totalSubtitleSegmentDuration > 0) {
while (index < segmentList.length && totalSubtitleSegmentDuration > 0) {
totalSubtitleSegmentDuration = Math.round(totalSubtitleSegmentDuration * 1000) / 1000;
if (videoSegments[index].discontinuity) {
newSegmentList.push(videoSegments[index])
if (segmentList[index].discontinuity) {
if (!segmentList[index].vodTransition) {
newSegmentList.push(segmentList[index])
}
index += 1;
continue;
}
Expand All @@ -1171,7 +1217,7 @@ class HLSVod {
const params = new URLSearchParams();
const startTime = segment.duration - totalSubtitleSegmentDuration;
const consumedVideoDuration = leftover.consumedVideoDuration ? leftover.consumedVideoDuration : 0
const endTime = startTime + Math.min(videoSegments[index].duration, totalSubtitleSegmentDuration) - consumedVideoDuration;
const endTime = startTime + Math.min(segmentList[index].duration, totalSubtitleSegmentDuration) - consumedVideoDuration;

if (!useDummyUrl) {
params.append("vtturi", segment.uri)
Expand All @@ -1193,22 +1239,22 @@ class HLSVod {
if (leftover.duration) {
newSegment.duration = leftover.duration + leftover.consumedVideoDuration;
totalSubtitleSegmentDuration -= leftover.duration;
if (leftover.duration + leftover.consumedVideoDuration === videoSegments[index].duration) {
if (leftover.duration + leftover.consumedVideoDuration === segmentList[index].duration) {
allVideoDurationUsed = true;
}
leftover = {};
}
else if (videoSegments[index].duration < totalSubtitleSegmentDuration) {
newSegment.duration = videoSegments[index].duration;
totalSubtitleSegmentDuration -= videoSegments[index].duration;
else if (segmentList[index].duration < totalSubtitleSegmentDuration) {
newSegment.duration = segmentList[index].duration;
totalSubtitleSegmentDuration -= segmentList[index].duration;
allVideoDurationUsed = true;
} else if (videoSegments[index].duration === totalSubtitleSegmentDuration) {
} else if (segmentList[index].duration === totalSubtitleSegmentDuration) {
newSegment.duration = totalSubtitleSegmentDuration;
totalSubtitleSegmentDuration = 0;
allVideoDurationUsed = true;
} else {
leftover = {
duration: videoSegments[index].duration - totalSubtitleSegmentDuration,
duration: segmentList[index].duration - totalSubtitleSegmentDuration,
previousSegmentUri: params,
consumedVideoDuration: totalSubtitleSegmentDuration
}
Expand Down Expand Up @@ -1677,6 +1723,7 @@ class HLSVod {
});
if (timeToRemove) {
totalSeqDur -= timeToRemove;
totalSeqDur = fixedNumber(totalSeqDur);
totalRemovedSegments++;
shiftedSegmentsCount++;
}
Expand All @@ -1701,7 +1748,7 @@ class HLSVod {
const langs = Object.keys(segments[groupId]);
langs.forEach((lang) => {
let seg = _sequence[groupId][lang].pop();
if (groupId === groupId && lang === firstLanguage) {
if (groupId === firstGroupId && lang === firstLanguage) {
timeToRemove = seg.duration;
}
});
Expand Down Expand Up @@ -1817,12 +1864,12 @@ class HLSVod {
if (type === "audio") {
this.deltaTimesAudio.push({
interval: interval,
position: positionIncrement ? lastPosition + tpi : lastPosition,
position: positionIncrement ? fixedNumber(lastPosition + tpi) : (lastPosition),
});
} else if (type === "subtitle") {
this.deltaTimesSubtitle.push({
interval: interval,
position: positionIncrement ? lastPosition + tpi : lastPosition,
position: positionIncrement ? fixedNumber(lastPosition + tpi) : (lastPosition),
});
}
if (positionIncrement) {
Expand Down Expand Up @@ -2146,7 +2193,23 @@ class HLSVod {


const result = this.generateSmallerSubtitleSegments(fakeSubtileSegment, offset, 0, true, false, 0)
this.subtitleSegments[this.DUMMY_DEFAULT_SUBTITLE_GROUP_ID][this.DUMMY_DEFAULT_SUBTITLE_LANGUAGE] = this.subtitleSegments[this.DUMMY_DEFAULT_SUBTITLE_GROUP_ID][this.DUMMY_DEFAULT_SUBTITLE_LANGUAGE].concat(result.newSegments)
this.subtitleSegments[this.DUMMY_DEFAULT_SUBTITLE_GROUP_ID][this.DUMMY_DEFAULT_SUBTITLE_LANGUAGE] = this.subtitleSegments[this.DUMMY_DEFAULT_SUBTITLE_GROUP_ID][this.DUMMY_DEFAULT_SUBTITLE_LANGUAGE].concat(result.newSegments);
// If the expected tracks are set and are not filled with new source segments, then let them have dummy segments
if (this.expectedSubtitleTracks.length && this.expectedSubtitleTracks.length > 0) {
for (let i = 0; i < this.expectedSubtitleTracks.length; i++) {
const track = this.expectedSubtitleTracks[i];
const [numSegsAfterVT, hasVT] = inspectForVodTransition(this.subtitleSegments[this.DEFAULT_SUBTITLE_GROUP_ID][track.language]);
// Case: Loading vod after
if (numSegsAfterVT === 0 && hasVT) {
this.subtitleSegments[this.DEFAULT_SUBTITLE_GROUP_ID][track.language] = this.subtitleSegments[this.DEFAULT_SUBTITLE_GROUP_ID][track.language].concat(result.newSegments);
}
// Case: Loading vod first time
if (this.subtitleSegments[this.DEFAULT_SUBTITLE_GROUP_ID][track.language].length === 0) {
const emptySubtitleSegList = this.subtitleSegments[this.DUMMY_DEFAULT_SUBTITLE_GROUP_ID][this.DUMMY_DEFAULT_SUBTITLE_LANGUAGE];
this.subtitleSegments[this.DEFAULT_SUBTITLE_GROUP_ID][track.language] = emptySubtitleSegList;
}
}
}
}

if (!this.sequenceAlwaysContainNewSegments) {
Expand Down
2 changes: 1 addition & 1 deletion spec/hlsvod_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2352,7 +2352,7 @@ describe("HLSVod serializing", () => {
return megabytes.toFixed(2);
}
const expectedJSONSizeInMBFull = "5.29";
const expectedJSONSizeInMBPartial = "0.39";
const expectedJSONSizeInMBPartial = "0.38";
const expectedJSONSizeInMBFullAgain = expectedJSONSizeInMBFull;
const expectedMseqCountFull = 146;
const expectedMseqCountPartial = 0;
Expand Down
56 changes: 51 additions & 5 deletions spec/hlsvod_subtitles_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("HLSVod with subtitles", () => {
sequenceAlwaysContainNewSegments: true
}

describe("", () => {
describe("can handle", () => {
let mockMasterManifestLongSegments;
let mockMediaManifestLongSegments;
let mockAudioManifestLongSegments;
Expand All @@ -49,6 +49,8 @@ describe("HLSVod with subtitles", () => {
let mockMediaPlaylistUnevenSubVideoSegments;
let mockSubtitlePlaylistUnevenSubVideoSegments;

let mockHlsDemuxWithPreroll;

beforeEach(() => {
mockMasterManifestLongSegments = function () {
return fs.createReadStream("testvectors/hls_subs/master.m3u8");
Expand Down Expand Up @@ -123,6 +125,21 @@ describe("HLSVod with subtitles", () => {
return fs.createReadStream(`testvectors/hls_subs3/subs/sub.m3u8`);
};

mockHlsDemuxWithPreroll = {
master: () => {
return fs.createReadStream("testvectors/hls_subs7_demux/master.m3u8");
},
media: (bw) => {
return fs.createReadStream(`testvectors/hls_subs7_demux/media7070700.m3u8`);
},
audio: (g, l) => {
return fs.createReadStream(`testvectors/hls_subs7_demux/audio.m3u8`);
},
subtitle: (g, l) => {
return fs.createReadStream(`testvectors/hls_subs7_demux/subtitle.m3u8`);
},
};

});

it("returns the correct number of bandwidths", (done) => {
Expand Down Expand Up @@ -272,10 +289,10 @@ describe("HLSVod with subtitles", () => {
const m3u8_2 = mockVod2.getLiveMediaSubtitleSequences(0, "textstream", "sv", 1);
const subStrings = m3u8.split("\n")
const subStrings2 = m3u8_2.split("\n")
expect(subStrings[33]).toEqual("https://vod.streaming.a2d.tv/3e542405-583b-4edc-93ab-eca86427d148/ab92a690-62de-11ed-aa51-c96fb4f9434f_20337209.ism/hls/ab92a690-62de-11ed-aa51-c96fb4f9434f_20337209-textstream_swe=1000-693.webvtt");
expect(subStrings2[32]).toEqual("#EXT-X-DISCONTINUITY")
expect(subStrings2[33]).toEqual("#EXTINF:3.000,");
expect(subStrings2[34]).toEqual("https://d3t8zrj2x5ol3r.cloudfront.net/u/file~text_vtt~dummy.vtt/1/s/webvtt.vtt");
expect(subStrings[35]).toEqual("https://vod.streaming.a2d.tv/3e542405-583b-4edc-93ab-eca86427d148/ab92a690-62de-11ed-aa51-c96fb4f9434f_20337209.ism/hls/ab92a690-62de-11ed-aa51-c96fb4f9434f_20337209-textstream_swe=1000-693.webvtt");
expect(subStrings2[34]).toEqual("#EXT-X-DISCONTINUITY")
expect(subStrings2[35]).toEqual("#EXTINF:3.000,");
expect(subStrings2[36]).toEqual("https://d3t8zrj2x5ol3r.cloudfront.net/u/file~text_vtt~dummy.vtt/1/s/webvtt.vtt");
done();
});
});
Expand Down Expand Up @@ -358,6 +375,35 @@ describe("HLSVod with subtitles", () => {
done();
});
});
it("deltaTimes and playheadPos, for audio- and subtitletracks with alwaysNewSegments(true)", (done) => {
const bool = 1;
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, 0, 0*1000, null, {
dummySubtitleEndpoint: "/dummysubs.vtt",
subtitleSliceEndpoint: "/subtitlevtt.vtt",
shouldContainSubtitles: true,
expectedSubtitleTracks: [ { language: "fr", name: "french" }, { language: "sv", name: "Swedish" }
],//subtitleTracks,
sequenceAlwaysContainNewSegments: true,
forcedDemuxMode: true
});
mockVod.load(mockHlsDemuxWithPreroll.master, mockHlsDemuxWithPreroll.media, mockHlsDemuxWithPreroll.audio, mockHlsDemuxWithPreroll.subtitle, )
.then(() => {
const deltaV = mockVod.getDeltaTimes()
const deltaS = mockVod.getDeltaTimes("subtitle")
const deltaA = mockVod.getDeltaTimes("audio")
const playheadPosV = mockVod.getPlayheadPositions()
const playheadPosS = mockVod.getPlayheadPositions("subtitle")
const playheadPosA = mockVod.getPlayheadPositions("audio")
expect(deltaV).not.toEqual(deltaS);
expect(deltaA.length).toBeGreaterThan(deltaS.length);
expect(deltaA.slice(0, 132)).toEqual(deltaS.slice(0, 132)); // Last 2 segments are expected to be different
expect(deltaA.slice(132)).not.toEqual(deltaS.slice(132));
expect(playheadPosV).not.toEqual(playheadPosS);
expect(playheadPosA.slice(0, 132)).toEqual(playheadPosS.slice(0, 132));
expect(playheadPosA.slice(132)).not.toEqual(playheadPosS.slice(132));
done();
});
});
it("deltaTimes and playheadPos, alwaysNewSegments(true)", (done) => {
const bool = 1;
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, 0, 0, null, hlsOptsAlwaysNewSegmentsTrue);
Expand Down
Loading

0 comments on commit fd9cabf

Please sign in to comment.