Skip to content

Commit

Permalink
Fix: reload() handle PDT timestamps alignment (#124)
Browse files Browse the repository at this point in the history
* fix: reload() handle PDT timestamps alignment

* add unittest for new reload PDT alignment
  • Loading branch information
Nfrederiksen authored Oct 15, 2024
1 parent 2baaa46 commit 45489f9
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 16 deletions.
125 changes: 110 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,13 @@ class HLSVod {
*/
reload(mediaSeqNo, additionalSegments, additionalAudioSegments, insertAfter) {
return new Promise((resolve, reject) => {

const allBandwidths = this.getBandwidths();

if (!insertAfter) {
// First handle case where we reload segments with Program Date time positions.
let newTimeOffset = this._getLastTimelinePositionVideo(additionalSegments);
let newTimeOffsetAudio = this._getLastTimelinePositionAudio(additionalAudioSegments);
// If there is anything to slice
if (mediaSeqNo > 0) {
let targetUri = "";
Expand Down Expand Up @@ -589,21 +594,31 @@ class HLSVod {

// Find nearest BW in SFL and prepend them to the corresponding segments bandwidth
allBandwidths.forEach((bw) => {
if (newTimeOffset > 0) {
let totalTimelinePos = 0;
for (let i = 0; i < this.segments[bw].length; i++) {
const segment = this.segments[bw][i];
if (segment.duration) {
totalTimelinePos += segment.duration * 1000;
this.segments[bw][i].timelinePosition = newTimeOffset + totalTimelinePos;
}
}
}
let nearestBw = this._getNearestBandwidthInList(bw, Object.keys(additionalSegments));
this.segments[bw] = additionalSegments[nearestBw].concat(this.segments[bw]);
});

if (!this._isEmpty(this.audioSegments) && additionalAudioSegments) {
const groupIdsInVod = this.getAudioGroups();
const groupIdsInSegments = Object.keys(additionalAudioSegments)
const groupIdsInSegments = Object.keys(additionalAudioSegments);

for (let i = 0; i < groupIdsInSegments.length; i++) {
let groupIdForVod = groupIdsInSegments[i];
let indexOfGroupId = groupIdsInVod.indexOf(groupIdsInSegments[i]);
if (indexOfGroupId < 0) {
groupIdForVod = groupIdsInVod[0]
groupIdForVod = groupIdsInVod[0];
} else {
groupIdForVod = groupIdsInVod[indexOfGroupId]
groupIdForVod = groupIdsInVod[indexOfGroupId];
}

const langsInVod = this.getAudioLangsForAudioGroup(groupIdForVod);
Expand All @@ -613,16 +628,31 @@ class HLSVod {
let langForVod = langsInSegment[j];
let indexOfLang = langsInVod.indexOf(langsInSegment[j]);
if (indexOfLang < 0) {
langForVod = langsInVod[0]
langForVod = langsInVod[0];
} else {
langForVod = langsInVod[indexOfLang]
langForVod = langsInVod[indexOfLang];
}
if (newTimeOffsetAudio > 0) {
let totalTimelinePos = 0;
for (let i = 0; i < this.audioSegments[groupIdForVod][langForVod].length; i++) {
const segment = this.audioSegments[groupIdForVod][langForVod][i];
if (segment.duration) {
totalTimelinePos += segment.duration * 1000;
this.audioSegments[groupIdForVod][langForVod][i].timelinePosition = newTimeOffsetAudio + totalTimelinePos;
}
}
}
this.audioSegments[groupIdForVod][langForVod] = additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]].concat(this.audioSegments[groupIdForVod][langForVod]);

this.audioSegments[groupIdForVod][langForVod] = additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]].concat(
this.audioSegments[groupIdForVod][langForVod]
);
}
}
}
} else {
// First handle case where we reload segments with Program Date time positions.
let newTimeOffset = this._getLastTimelinePositionVideo(this.segments);
let newTimeOffsetAudio = this._getLastTimelinePositionAudio(this.audioSegments);
// Slice Video segments
if (mediaSeqNo >= 0) {
let size = this.mediaSequences[mediaSeqNo].segments[allBandwidths[0]].length;
let targetUri = this.mediaSequences[mediaSeqNo].segments[allBandwidths[0]][0].uri;
Expand All @@ -635,7 +665,7 @@ class HLSVod {
}
allBandwidths.forEach((bw) => (this.segments[bw] = this.segments[bw].slice(targetPos, targetPos + size)));
}

// Slice Audio segments
if (!this._isEmpty(this.audioSegments) && additionalAudioSegments) {
const groupIds = this.getAudioGroups();
const lang = this.getAudioLangsForAudioGroup(groupIds[0])[0];
Expand All @@ -659,6 +689,20 @@ class HLSVod {
}
}
}
// Update Program Date Time positions in the additional segments video
if (newTimeOffset > 0) {
const allAdditionalSegmentsBandwidths = Object.keys(additionalSegments);
allAdditionalSegmentsBandwidths.forEach((bw) => {
let totalTimelinePos = 0;
for (let i = 0; i < additionalSegments[bw].length; i++) {
const segment = additionalSegments[bw][i];
if (segment.duration) {
totalTimelinePos += segment.duration * 1000;
additionalSegments[bw][i].timelinePosition = newTimeOffset + totalTimelinePos;
}
}
});
}

allBandwidths.forEach((bw) => {
let nearestBw = this._getNearestBandwidthInList(bw, Object.keys(additionalSegments));
Expand All @@ -667,15 +711,15 @@ class HLSVod {

if (!this._isEmpty(this.audioSegments) && additionalAudioSegments) {
const groupIdsInVod = this.getAudioGroups();
const groupIdsInSegments = Object.keys(additionalAudioSegments)
const groupIdsInSegments = Object.keys(additionalAudioSegments);

for (let i = 0; i < groupIdsInSegments.length; i++) {
let groupIdForVod = groupIdsInSegments[i];
let indexOfGroupId = groupIdsInVod.indexOf(groupIdsInSegments[i]);
if (indexOfGroupId < 0) {
groupIdForVod = groupIdsInVod[0]
groupIdForVod = groupIdsInVod[0];
} else {
groupIdForVod = groupIdsInVod[indexOfGroupId]
groupIdForVod = groupIdsInVod[indexOfGroupId];
}

const langsInVod = this.getAudioLangsForAudioGroup(groupIdForVod);
Expand All @@ -685,12 +729,23 @@ class HLSVod {
let langForVod = langsInSegment[j];
let indexOfLang = langsInVod.indexOf(langsInSegment[j]);
if (indexOfLang < 0) {
langForVod = langsInVod[0]
langForVod = langsInVod[0];
} else {
langForVod = langsInVod[indexOfLang]
langForVod = langsInVod[indexOfLang];
}
if (newTimeOffsetAudio > 0) {
let totalTimelinePos = 0;
for (let x = 0; x < additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]].length; x++) {
const segment =additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]][x];
if (segment.duration) {
totalTimelinePos += segment.duration * 1000;
additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]][x].timelinePosition = newTimeOffsetAudio + totalTimelinePos;
}
}
}
this.audioSegments[groupIdForVod][langForVod] = this.audioSegments[groupIdForVod][langForVod].concat(additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]]);

this.audioSegments[groupIdForVod][langForVod] = this.audioSegments[groupIdForVod][langForVod].concat(
additionalAudioSegments[groupIdsInSegments[i]][langsInSegment[j]]
);
}
}
}
Expand Down Expand Up @@ -1569,6 +1624,7 @@ class HLSVod {
}
return videoSequences;
}

generateSequencesTypeBExtraMedia(segments, firstGroupId, firstLanguage, type) {
let totalRemovedDiscTags = 0;
let totalRemovedSegments = 0;
Expand Down Expand Up @@ -3337,6 +3393,45 @@ class HLSVod {
}
}

_getLastTimelinePositionVideo(ObjectOfSegments) {
let newTimeOffset = 0;
if (!this._isEmpty(ObjectOfSegments)) {
// For Video
if (ObjectOfSegments[Object.keys(ObjectOfSegments)[0]].length > 0) {
for (let i = ObjectOfSegments[Object.keys(ObjectOfSegments)[0]].length - 1; i >= 0; i--) {
if (ObjectOfSegments[Object.keys(ObjectOfSegments)[0]][i].uri && !ObjectOfSegments[Object.keys(ObjectOfSegments)[0]][i].timelinePosition) {
break;
}
if (ObjectOfSegments[Object.keys(ObjectOfSegments)[0]][i].timelinePosition) {
newTimeOffset = ObjectOfSegments[Object.keys(ObjectOfSegments)[0]][i].timelinePosition;
break;
}
}
}
}
return newTimeOffset;
}

_getLastTimelinePositionAudio(ObjectOfSegments) {
let newTimeOffsetAudio = 0;
if (!this._isEmpty(ObjectOfSegments)) {
// For Audio
const groupIdsInSegments = Object.keys(ObjectOfSegments);
const langsInSegment = Object.keys(ObjectOfSegments[groupIdsInSegments[0]]);
if (!this._isEmpty(langsInSegment) && ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]].length > 0) {
for (let i = ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]].length - 1; i >= 0; i--) {
if (ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]][i].uri && !ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]][i].timelinePosition) {
break;
}
if (ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]][i].timelinePosition) {
newTimeOffsetAudio = ObjectOfSegments[groupIdsInSegments[0]][langsInSegment[0]][i].timelinePosition;
break;
}
}
}
}
return newTimeOffsetAudio;
}
}

module.exports = HLSVod;
51 changes: 50 additions & 1 deletion spec/hlsvod_audio_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,59 @@ describe("HLSVod with demuxed audio", () => {
});
});

it("can reload at the middle of a HLSVod, and set PDT Timestamps correctly if reload media has it", (done) => {
let vod1segments = {};
let vod1AudioSegments = {};
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, Date.now(), 0, null, null);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8");

mockVod
.load(mockMasterManifest1, mockMediaManifest1, mockAudioManifest1)
.then(() => {
vod1segments = mockVod.getLiveMediaSequenceSegments(1);
vod1AudioSegments = mockVod.getLiveAudioSequenceSegments(1);
Object.keys(vod1segments).forEach((bw) => vod1segments[bw].push({ discontinuity: true }));
const groupIds = Object.keys(vod1AudioSegments);
for (let i = 0; i < groupIds.length; i++) {
const groupId = groupIds[i];
const langs = Object.keys(vod1AudioSegments[groupId])
for (let j = 0; j < langs.length; j++) {
const lang = langs[j];
vod1AudioSegments[groupId][lang].push({ discontinuity: true });
}
}
})
.then(() => {
mockVod2.load(mockMasterManifest2, mockMediaManifest2, mockAudioManifest2).then(() => {
let bottomSegmentPreReload =
mockVod2.getLiveMediaSequenceSegments(6)["401000"][mockVod2.getLiveMediaSequenceSegments(6)["401000"].length - 1];
let bottomAudioSegmentPreReload =
mockVod2.getLiveAudioSequenceSegments(6)["aac"]["en"][mockVod2.getLiveAudioSequenceSegments(6)["aac"]["en"].length - 1];
mockVod2.reload(6, vod1segments, vod1AudioSegments).then(() => {
let size = mockVod2.getLiveMediaSequenceSegments(1)["401000"].length;
expect(mockVod2.getLiveMediaSequenceSegments(1)["401000"][size - 1]).toEqual(bottomSegmentPreReload);
let sizeAudio = mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"].length;
expect(mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"][sizeAudio - 1]).toEqual(bottomAudioSegmentPreReload);
const lastSegmentItem = mockVod2.getLiveMediaSequenceSegments(1)["401000"][mockVod2.getLiveMediaSequenceSegments(1)["401000"].length - 1];
const secondLastSegmentItem = mockVod2.getLiveMediaSequenceSegments(1)["401000"][mockVod2.getLiveMediaSequenceSegments(1)["401000"].length - 3];
const differenceInPDT = lastSegmentItem.timelinePosition - secondLastSegmentItem.timelinePosition;
const lastSegmentItemDurationMs = lastSegmentItem.duration * 1000;
expect(differenceInPDT).toEqual(lastSegmentItemDurationMs);
const lastSegmentItemAudio = mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"][mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"].length - 1];
const secondLastSegmentItemAudio = mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"][mockVod2.getLiveAudioSequenceSegments(1)["aac"]["en"].length - 3];
const differenceInPDTAudio = lastSegmentItemAudio.timelinePosition - secondLastSegmentItemAudio.timelinePosition;
const lastSegmentItemDurationMsAudio = lastSegmentItemAudio.duration * 1000;
expect(differenceInPDTAudio).toEqual(lastSegmentItemDurationMsAudio);
done();
});
});
});
});

it("can reload at the middle of a HLSVod, and insert segments after live point", (done) => {
let vod1segments = {};
let vod1AudioSegments = {};
mockVod = new HLSVod("http://mock.com/mock.m3u8");
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, Date.now(), 0, null, null);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8");

mockVod
Expand Down

0 comments on commit 45489f9

Please sign in to comment.