• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Eyevinn / hls-vodtolive / #925

04 Jul 2025 01:53PM UTC coverage: 91.251% (+0.4%) from 90.82%
#925

push

Nfrederiksen
specs: add test for fix

1232 of 1508 branches covered (81.7%)

Branch coverage included in aggregate %.

40 of 40 new or added lines in 2 files covered. (100.0%)

4 existing lines in 1 file now uncovered.

4692 of 4984 relevant lines covered (94.14%)

3384.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.04
/utils.js
1
const fetch = require("node-fetch");
1✔
2
const { AbortController } = require("abort-controller");
1✔
3
const debug = require("debug")("hls-vodtolive");
1✔
4

5
function keysToM3u8(keys) {
6
  let m3u8 = "";
34✔
7
  for (const keyFormat of Object.keys(keys)) {
34✔
8
    const key = keys[keyFormat];
36✔
9
    m3u8 += `#EXT-X-KEY:METHOD=${key.method}`;
36✔
10
    m3u8 += key.uri ? `,URI=${key.uri}` : "";
36!
11
    m3u8 += key.iv ? `,IV=${key.iv}` : "";
36✔
12
    m3u8 += key.keyId ? `,KEYID=${key.keyId}` : "";
36✔
13
    m3u8 += key.keyFormatVersions ? `,KEYFORMATVERSIONS=${key.keyFormatVersions}` : "";
36!
14
    m3u8 += key.keyFormat ? `,KEYFORMAT=${key.keyFormat}` : "";
36!
15
    m3u8 += "\n";
36✔
16
  }
17
  return m3u8;
34✔
18
}
19

20
function daterangeAttribute (key, attr) {
21
  if (key === "planned-duration" || key === "duration") {
21✔
22
    return key.toUpperCase() + "=" + `${attr.toFixed(3)}`;
1✔
23
  } else {
24
    return key.toUpperCase() + "=" + `"${attr}"`;
20✔
25
  }
26
}
27

28
function urlResolve(from, to) {
29
  const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
16,415✔
30
  if (resolvedUrl.protocol === 'resolve:') {
16,415!
31
    // `from` is a relative URL.
32
    const { pathname, search, hash } = resolvedUrl;
×
33
    return pathname + search + hash;
×
34
  }
35
  return resolvedUrl.toString();
16,415✔
36
}
37

38
function insertCueTagData(v, i, len, nextSegment, m3u8Str) {  
39
  if (v.cue) {
8!
40
    if (v.cue.out) {
8✔
41
      if (v.cue.scteData) {
4!
42
        m3u8Str += "#EXT-OATCLS-SCTE35:" + v.cue.scteData + "\n";
×
43
      }
44
      if (v.cue.assetData) {
4!
45
        m3u8Str += "#EXT-X-ASSET:" + v.cue.assetData + "\n";
×
46
      }
47
      m3u8Str += "#EXT-X-CUE-OUT:DURATION=" + v.cue.duration + "\n";
4✔
48
    }
49
    if (v.cue.cont) {
8!
50
      if (v.cue.scteData) {
×
51
        m3u8Str += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + v.cue.cont + ",Duration=" + v.cue.duration + ",SCTE35=" + v.cue.scteData + "\n";
×
52
      } else {
53
        m3u8Str += "#EXT-X-CUE-OUT-CONT:" + v.cue.cont + "/" + v.cue.duration + "\n";
×
54
      }
55
    }
56
    if (v.cue.in) {
8✔
57
      if (nextSegment && nextSegment.discontinuity && i + 1 == len - 1) {
4!
58
        // Do not add a closing cue-in if next is not a segment and last one in the list
59
      } else {
60
        m3u8Str += "#EXT-X-CUE-IN" + "\n"; 
4✔
61
      }
62
    }
63
  }
64
  return m3u8Str;
8✔
65
};
66

67
function segToM3u8(vin, i, len, nextSegment, previousSegment) {
68
  let m3u8 = "";
1,625✔
69
  
70
  const v = JSON.parse(JSON.stringify(vin));
1,625✔
71

72
  if (previousSegment != null) {
1,625✔
73
    if (previousSegment.discontinuity) {
1,507✔
74
      if (v.initSegment) {
138✔
75
        let byteRangeStr = "";
64✔
76
        if (v.initSegmentByteRange) {
64!
77
          byteRangeStr = `,BYTERANGE="${v.initSegmentByteRange}"`;
×
78
        }
79
        if (v.cue) {
64✔
80
          m3u8 = insertCueTagData(v, i, len, nextSegment, m3u8);
8✔
81
          v.cue = null;
8✔
82
        }
83
        m3u8 += `#EXT-X-MAP:URI="${v.initSegment}"${byteRangeStr}\n`;
64✔
84
      }
85
      if (v.keys) {
138✔
86
        m3u8 += keysToM3u8(v.keys);
19✔
87
      }  
88
    }
89
  }
90

91
  if (i === 0) {
1,625✔
92
    if (v.initSegment) {
118✔
93
      let byteRangeStr = "";
38✔
94
      if (v.initSegmentByteRange) {
38✔
95
        byteRangeStr = `,BYTERANGE="${v.initSegmentByteRange}"`;
2✔
96
      }
97
      if (v.cue) {
38!
98
        m3u8 = insertCueTagData(v, i, len, nextSegment, m3u8);
×
99
        v.cue = null;
×
100
      }
101
      m3u8 += `#EXT-X-MAP:URI="${v.initSegment}"${byteRangeStr}\n`;
38✔
102
    }
103
    if (v.keys) {
118✔
104
      m3u8 += keysToM3u8(v.keys);
15✔
105
    }
106
  }
107

108
  if (!v.discontinuity) {
1,625✔
109
    if (v.daterange) {
1,484✔
110
      const dateRangeAttributes = Object.keys(v.daterange)
3✔
111
        .map((key) => daterangeAttribute(key, v.daterange[key]))
19✔
112
        .join(",");
113
      if ((nextSegment && !nextSegment.timelinePosition) && v.daterange["start-date"]) {
3✔
114
        m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + v.daterange["start-date"] + "\n";
1✔
115
      }
116
      m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
3✔
117
    }
118

119
    if (v.cue && v.cue.out) {
1,484✔
120
      if (v.cue.scteData) {
6!
121
        m3u8 += "#EXT-OATCLS-SCTE35:" + v.cue.scteData + "\n";
×
122
      }
123
      if (v.cue.assetData) {
6!
124
        m3u8 += "#EXT-X-ASSET:" + v.cue.assetData + "\n";
×
125
      }
126
      m3u8 += "#EXT-X-CUE-OUT:DURATION=" + v.cue.duration + "\n";
6✔
127
    }
128
    if (v.cue && v.cue.cont) {
1,484✔
129
      if (v.cue.scteData) {
5!
130
        m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + v.cue.cont + ",Duration=" + v.cue.duration + ",SCTE35=" + v.cue.scteData + "\n";
×
131
      } else {
132
        m3u8 += "#EXT-X-CUE-OUT-CONT:" + v.cue.cont + "/" + v.cue.duration + "\n";
5✔
133
      }
134
    }
135
    //console.log("my v::",JSON.stringify(v,null, 2), 19993.66, "..aacuein")
136
    if (v.cue && v.cue.in) {
1,484✔
137
      if (nextSegment && nextSegment.discontinuity && i + 1 == len - 1) {
7!
138
        // Do not add a closing cue-in if next is not a segment and last one in the list
139
      } else {
140
        m3u8 += "#EXT-X-CUE-IN" + "\n";
7✔
141
      }
142
    }
143
    if (v.uri) {
1,484✔
144
      if (v.timelinePosition) {
1,478✔
145
        const d = new Date(v.timelinePosition);
62✔
146
        m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + d.toISOString() + "\n";
62✔
147
      }
148
      m3u8 += "#EXTINF:" + v.duration.toFixed(3) + ",\n";
1,478✔
149
      if (v.byteRange) {
1,478✔
150
        m3u8 += `#EXT-X-BYTERANGE:${v.byteRange}\n`;
31✔
151
      }
152
      m3u8 += v.uri + "\n";
1,478✔
153
    }
154
  } else {
155
    if (i != 0 && i != len - 1) {
141✔
156
      m3u8 += "#EXT-X-DISCONTINUITY\n";
123✔
157
      if (v.cue && v.cue.in) {
123!
158
        m3u8 += "#EXT-X-CUE-IN" + "\n";
×
159
      }
160
    }
161
    if (v.daterange && i != len - 1 && !(v.daterange["CLASS"] == "com.apple.hls.interstitial" && v.daterange["CUE"])) {
141!
162
      const dateRangeAttributes = Object.keys(v.daterange)
1✔
163
        .map((key) => daterangeAttribute(key, v.daterange[key]))
2✔
164
        .join(",");
165
      if (nextSegment && !nextSegment.timelinePosition && v.daterange["start-date"]) {
1!
166
        m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + v.daterange["start-date"] + "\n";
1✔
167
      }
168
      m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
1✔
169
    }
170
  }
171

172
  return m3u8;
1,625✔
173
}
174

175
async function fetchWithRetry(uri, opts, maxRetries, retryDelayMs, timeoutMs, debug) {
176
  if (!debug) {
4!
177
    var debug = (msg) => {
×
178
      if (process.env.ENVIRONMENT === "development") {
×
179
        console.log(msg);
×
180
      }
181
    };
182
  }
183
  let tryFetchCount = 0;
4✔
184
  const RETRY_LIMIT = maxRetries || 5;
4!
185
  const TIMEOUT_LIMIT = timeoutMs || 10 * 1000;
4!
186
  const RETRY_DELAY = retryDelayMs || 1000;
4!
187
  while (tryFetchCount < RETRY_LIMIT) {
4✔
188
    tryFetchCount++;
4✔
189
    debug(`Fetching URI -> ${uri}, attempt ${tryFetchCount} of ${maxRetries}`);
4✔
190
    const controller = new AbortController();
4✔
191
    const timeout = setTimeout(() => {
4✔
192
      `Request Timeout after (${TIMEOUT_LIMIT})ms @ ${uri}`;
×
193
      controller.abort();
×
194
    }, TIMEOUT_LIMIT);
195
    try {
4✔
196
      const fetchOpts = Object.assign({ signal: controller.signal }, opts);
4✔
197
      const response = await fetch(uri, fetchOpts);
4✔
198

199
      if (response.status >= 400 && response.status < 600) {
4!
200
        const msg = `Bad response from URI: ${uri}\nReturned Status Code: ${response.status}`;
×
201
        debug(msg);
×
202
        if (tryFetchCount === maxRetries) {
×
203
          return Promise.resolve(response);
×
204
        }
205
        debug(`Going to try fetch again in ${RETRY_DELAY}ms`);
×
206
        await timer(RETRY_DELAY);
×
207
        continue;
×
208
      }
209
      // Return Good response
210
      return Promise.resolve(response);
4✔
211
    } catch (err) {
212
      debug(`Node-Fetch Error on URI: ${uri}\nFull Error -> ${err}`);
×
213
      if (tryFetchCount === maxRetries) {
×
214
        return Promise.reject(err);
×
215
      }
216
      continue;
×
217
    } finally {
218
      clearTimeout(timeout);
4✔
219
    }
220
  }
221
}
222

223
function findIndexReversed(arr, fn) {
224
  const size = arr.length;
4,529✔
225
  for (let i = size - 1; i >= 0; i--) {
4,529✔
226
    const item = arr[i];
9,302✔
227
    const verdict = fn(item);
9,302✔
228
    if (verdict) {
9,302✔
229
      return i;
4,529✔
230
    }
231
  }
232
  return -1;
×
233
};
234

235
const findBottomSegItem = (arr) => {
1✔
236
  for (let i = arr.length - 1; i >= 0; i--) {
53✔
237
    if (arr[i].hasOwnProperty('duration') && arr[i].hasOwnProperty('uri') ) {
55✔
238
      return arr[i];
53✔
239
    }
240
  }
UNCOV
241
  return null;
×
242
}
243

244
const fixedNumber = (n) => {
1✔
245
  // Avoid JS floating point error
246
  let o = Math.round(n*100) / 100;
5,645✔
247
  return o;
5,645✔
248
}
249

250
const inspectForVodTransition = (list) => {
1✔
251
  let count = 0;
175✔
252
  let foundVodTransition = false;
175✔
253
  for (let i = 0; i < list.length; i++) {
175✔
254
    if (foundVodTransition) {
13,384✔
255
      count++;
3,774✔
256
    }
257
    if (list[i].vodTransition) {
13,384✔
258
      foundVodTransition = true;
61✔
259
    }
260
  }
261
  return [count, foundVodTransition];
175✔
262
};
263

264
const playlistItemWithInterstitialsMetadata = (pli) => {
1✔
265
  const daterange = pli.attributes.attributes.daterange;
122,418✔
266
  if (daterange && daterange["CLASS"] == "com.apple.hls.interstitial") {
122,418✔
267
    return true;
8✔
268
  }
269
  return false;
122,410✔
270
};
271

272
const appendHlsInterstitialLineWithCUE = (m3u8Str, data) => {
1✔
273
    // FOR LIVE AND CUED HLS-INTERSTITIAL TAGS, Place them at the bottom of each window
UNCOV
274
    if (data) {
×
275
      const dateRangeAttributes = Object.keys(data)
×
276
        .map((key) => daterangeAttribute(key, data[key]))
×
277
        .join(",");
UNCOV
278
        m3u8Str += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
×
279
    }
UNCOV
280
    return m3u8Str;
×
281
}
282

283
module.exports = {
1✔
284
  daterangeAttribute,
285
  keysToM3u8,
286
  segToM3u8,
287
  urlResolve,
288
  fetchWithRetry,
289
  findIndexReversed,
290
  findBottomSegItem,
291
  fixedNumber,
292
  inspectForVodTransition,
293
  playlistItemWithInterstitialsMetadata,
294
  appendHlsInterstitialLineWithCUE
295
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc