incyclist-services 1.7.90 → 1.7.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/lib/cjs/activities/base/api/fitconvert/fitconvert.js +2 -2
  2. package/lib/cjs/activities/base/repo/db.js +2 -2
  3. package/lib/cjs/activities/base/utils/activity.js +1 -0
  4. package/lib/cjs/apps/base/api/velohero/api.js +1 -1
  5. package/lib/cjs/apps/intervals/IntervalsAppConnection.js +1 -1
  6. package/lib/cjs/apps/komoot/KomootAppConnection.js +1 -1
  7. package/lib/cjs/apps/strava/StravaAppConnection.js +1 -1
  8. package/lib/cjs/apps/velohero/VeloHeroAppConnection.js +1 -1
  9. package/lib/cjs/autoupdate/api.js +15 -5
  10. package/lib/cjs/coaches/service.js +1 -2
  11. package/lib/cjs/devices/ride/service.js +1 -1
  12. package/lib/cjs/maps/MapArea/MapArea.js +1 -2
  13. package/lib/cjs/maps/MapArea/service.js +1 -0
  14. package/lib/cjs/ride/display/service.js +1 -1
  15. package/lib/cjs/ride/route/FreeRideDisplayService.js +1 -2
  16. package/lib/cjs/ride/route/RLVDisplayService.js +7 -17
  17. package/lib/cjs/routes/base/parsers/incyclist.js +1 -1
  18. package/lib/cjs/routes/base/parsers/xml.js +1 -1
  19. package/lib/cjs/routes/base/utils/points.js +3 -3
  20. package/lib/cjs/routes/list/loaders/db.js +1 -1
  21. package/lib/cjs/ui/service.js +8 -3
  22. package/lib/cjs/video/Mp4BoxParser.js +145 -0
  23. package/lib/cjs/video/VideoConversion.js +1 -1
  24. package/lib/cjs/video/index.js +2 -0
  25. package/lib/cjs/video/resolvePlaybackUrl.js +23 -0
  26. package/lib/cjs/workouts/list/cards/WorkoutCard.js +1 -1
  27. package/lib/cjs/workouts/list/loaders/db.js +1 -1
  28. package/lib/esm/activities/base/api/fitconvert/fitconvert.js +2 -2
  29. package/lib/esm/activities/base/repo/db.js +2 -2
  30. package/lib/esm/activities/base/utils/activity.js +1 -0
  31. package/lib/esm/apps/base/api/velohero/api.js +1 -1
  32. package/lib/esm/apps/intervals/IntervalsAppConnection.js +1 -1
  33. package/lib/esm/apps/komoot/KomootAppConnection.js +1 -1
  34. package/lib/esm/apps/strava/StravaAppConnection.js +1 -1
  35. package/lib/esm/apps/velohero/VeloHeroAppConnection.js +1 -1
  36. package/lib/esm/autoupdate/api.js +15 -5
  37. package/lib/esm/coaches/service.js +1 -2
  38. package/lib/esm/devices/ride/service.js +1 -1
  39. package/lib/esm/maps/MapArea/MapArea.js +1 -2
  40. package/lib/esm/maps/MapArea/service.js +1 -0
  41. package/lib/esm/ride/display/service.js +1 -1
  42. package/lib/esm/ride/route/FreeRideDisplayService.js +1 -2
  43. package/lib/esm/ride/route/RLVDisplayService.js +8 -18
  44. package/lib/esm/routes/base/parsers/incyclist.js +1 -1
  45. package/lib/esm/routes/base/parsers/xml.js +1 -1
  46. package/lib/esm/routes/base/utils/points.js +3 -3
  47. package/lib/esm/routes/list/loaders/db.js +1 -1
  48. package/lib/esm/ui/service.js +8 -3
  49. package/lib/esm/video/Mp4BoxParser.js +141 -0
  50. package/lib/esm/video/VideoConversion.js +1 -1
  51. package/lib/esm/video/index.js +2 -0
  52. package/lib/esm/video/resolvePlaybackUrl.js +19 -0
  53. package/lib/esm/workouts/list/cards/WorkoutCard.js +1 -1
  54. package/lib/esm/workouts/list/loaders/db.js +1 -1
  55. package/lib/types/autoupdate/api.d.ts +2 -1
  56. package/lib/types/ride/route/RLVDisplayService.d.ts +1 -0
  57. package/lib/types/video/Mp4BoxParser.d.ts +10 -0
  58. package/lib/types/video/index.d.ts +2 -0
  59. package/lib/types/video/resolvePlaybackUrl.d.ts +1 -0
  60. package/package.json +8 -6
@@ -60,14 +60,14 @@ let IncyclistFitConvertApi = (() => {
60
60
  url = response.data;
61
61
  }
62
62
  catch (error) {
63
- throw new Error(`convert failed: (phase convert), reason: ${error.message}`);
63
+ throw new Error(`convert failed: (phase convert), reason: ${error.message}`, { cause: error });
64
64
  }
65
65
  try {
66
66
  const response = await this.getApi().get(url, { responseType: 'arraybuffer' });
67
67
  return response.data;
68
68
  }
69
69
  catch (error) {
70
- throw new Error(`convert failed: (phase download), reason: ${error.message}`);
70
+ throw new Error(`convert failed: (phase download), reason: ${error.message}`, { cause: error });
71
71
  }
72
72
  }
73
73
  getApi() {
@@ -265,8 +265,8 @@ let ActivitiesRepository = (() => {
265
265
  if (typeof endPos !== 'number' && endPos?.value !== undefined) {
266
266
  endPos = this.getUnitConverter().convert(endPos.value, 'distance', { from: endPos.unit, to: 'm' });
267
267
  }
268
- if (result?.length > 0 && criteria?.endPos !== undefined) {
269
- result = result.filter(ai => ai.summary.endPos === criteria.endPos);
268
+ if (result?.length > 0 && endPos !== undefined) {
269
+ result = result.filter(ai => ai.summary.endPos === endPos);
270
270
  }
271
271
  return result;
272
272
  }
@@ -236,6 +236,7 @@ let Activity = (() => {
236
236
  }
237
237
  catch (err) {
238
238
  this.logError(err, 'upload');
239
+ error = err.message;
239
240
  }
240
241
  }
241
242
  this.markUploading(connectedApp, false);
@@ -38,7 +38,7 @@ class VeloHeroApi extends base_1.AppApiBase {
38
38
  const { response } = err;
39
39
  error = `HTTP error ${response.status}: ${response.statusText}`;
40
40
  }
41
- throw new Error(error);
41
+ throw new Error(error, { cause: err });
42
42
  }
43
43
  }
44
44
  getAccountType() {
@@ -122,7 +122,7 @@ let IntervalsAppConnection = (() => {
122
122
  return this.api;
123
123
  }
124
124
  initAuth() {
125
- let isInitialized = false;
125
+ let isInitialized;
126
126
  try {
127
127
  const auth = this.getCredentials();
128
128
  if (auth) {
@@ -119,7 +119,7 @@ let KomootAppConnection = (() => {
119
119
  return false;
120
120
  }
121
121
  checkAuth() {
122
- let isInitialized = false;
122
+ let isInitialized;
123
123
  let credentials;
124
124
  if (!this.getUserSettings().isInitialized) {
125
125
  return false;
@@ -120,7 +120,7 @@ let StravaAppConnection = (() => {
120
120
  return this.api;
121
121
  }
122
122
  initAuth() {
123
- let isInitialized = false;
123
+ let isInitialized;
124
124
  try {
125
125
  const auth = this.getCredentials();
126
126
  const clientId = this.getSecret('STRAVA_CLIENT_ID');
@@ -105,7 +105,7 @@ let VeloHeroAppConnection = (() => {
105
105
  return this.api;
106
106
  }
107
107
  initAuth() {
108
- let isInitialized = false;
108
+ let isInitialized;
109
109
  if (!this.getUserSettings().isInitialized) {
110
110
  return false;
111
111
  }
@@ -42,6 +42,7 @@ const service_1 = require("../base/service");
42
42
  const decorators_1 = require("../base/decorators");
43
43
  const UPDATE_SERVER_URL = 'UPDATE_SERVER_URL';
44
44
  const DEFAULT_DOMAIN = 'incyclist.com';
45
+ const UPDATE_CHECK_TIMEOUT_MS = 5000;
45
46
  let IncyclistUpdatesApi = (() => {
46
47
  let _classDecorators = [types_1.Singleton];
47
48
  let _classDescriptor;
@@ -96,10 +97,19 @@ let IncyclistUpdatesApi = (() => {
96
97
  const base = this.getSetting(UPDATE_SERVER_URL, defaultUrl);
97
98
  return base;
98
99
  }
99
- async _get(url, ...args) {
100
+ async _get(url, config = {}) {
100
101
  const api = this.getApi();
101
102
  const baseUrl = this.getBaseUrl();
102
- return await api.get(baseUrl + url, ...args);
103
+ return await api.get(baseUrl + url, { timeout: UPDATE_CHECK_TIMEOUT_MS, ...config });
104
+ }
105
+ isExpectedError(err) {
106
+ if (!err)
107
+ return false;
108
+ if (err.message === 'Network Error')
109
+ return true;
110
+ if (/timeout/i.test(err.message ?? ''))
111
+ return true;
112
+ return false;
103
113
  }
104
114
  async getLatestMacAppVersion() {
105
115
  try {
@@ -114,7 +124,7 @@ let IncyclistUpdatesApi = (() => {
114
124
  return { version, path, url, downloadUrl };
115
125
  }
116
126
  catch (err) {
117
- if (err.message !== 'Network Error') {
127
+ if (!this.isExpectedError(err)) {
118
128
  this.logError(err, 'getLatestMacAppVersion');
119
129
  }
120
130
  }
@@ -142,7 +152,7 @@ let IncyclistUpdatesApi = (() => {
142
152
  return { version, path, url, downloadUrl };
143
153
  }
144
154
  catch (err) {
145
- if (err.message !== 'Network Error') {
155
+ if (!this.isExpectedError(err)) {
146
156
  this.logError(err, 'getLatestLinuxAppVersion');
147
157
  }
148
158
  }
@@ -165,7 +175,7 @@ let IncyclistUpdatesApi = (() => {
165
175
  return { version, path, url, downloadUrl };
166
176
  }
167
177
  catch (err) {
168
- if (err.message !== 'Network Error') {
178
+ if (!this.isExpectedError(err)) {
169
179
  this.logError(err, 'getLatestWindowsAppVersion');
170
180
  }
171
181
  }
@@ -257,9 +257,8 @@ let CoachesService = (() => {
257
257
  if (!route)
258
258
  return;
259
259
  const prevPosition = coach.getPosition() || route.points[0];
260
- let nextPosition = undefined;
261
260
  if (distance > 0) {
262
- nextPosition = (0, route_1.getNextPosition)(route, { distance, prev: prevPosition });
261
+ const nextPosition = (0, route_1.getNextPosition)(route, { distance, prev: prevPosition });
263
262
  if (nextPosition) {
264
263
  const { slope } = nextPosition;
265
264
  coach.sendDeviceUpdate({ slope: slope * realityFactor / 100 });
@@ -1134,7 +1134,7 @@ let DeviceRideService = (() => {
1134
1134
  const adapters = this.getRideAdapters();
1135
1135
  const config = this.getDeviceConfiguration();
1136
1136
  const selectedDevices = selected ?? config.getSelectedDevices();
1137
- let enabledCapabilities = [];
1137
+ let enabledCapabilities;
1138
1138
  const duplicates = this.checkAntSameDeviceID(adapters);
1139
1139
  const toBeReplaced = duplicates.filter(dai => dai.udid === adapterInfo.udid).map(dai => dai.info.udid);
1140
1140
  if (this.simulatorEnforced) {
@@ -146,7 +146,6 @@ class MapArea {
146
146
  };
147
147
  const addOption = (newPath, res, reverse) => {
148
148
  const optWay = way;
149
- let path = [];
150
149
  if (newPath.length === 1) {
151
150
  addWaysCrossing(res);
152
151
  }
@@ -156,7 +155,7 @@ class MapArea {
156
155
  newPath[0].id === way.path.at(-1).id)) {
157
156
  addWaysCrossing(res);
158
157
  }
159
- path = newPath;
158
+ const path = newPath;
160
159
  if (reverse)
161
160
  path.reverse();
162
161
  res.push({ wayId: optWay.id, path });
@@ -172,6 +172,7 @@ let MapAreaService = (() => {
172
172
  let ts;
173
173
  for (let i = 0; i < 2; i++) {
174
174
  try {
175
+ ts = Date.now();
175
176
  const bounds = (0, utils_1.getBounds)(location?.lat, location?.lng, this.radius);
176
177
  const query = (0, utils_1.buildQuery)(consts_1.GET_WAYS_IN_AREA, bounds);
177
178
  const openmapData = await this.getOverpassAPI().query(query, DEFAULT_TIMEOUT);
@@ -873,7 +873,7 @@ let RideDisplayService = (() => {
873
873
  }
874
874
  simulatorPowerUp(mode, powerInc) {
875
875
  const simMode = mode.getSetting('mode');
876
- let request = {};
876
+ let request;
877
877
  if (simMode === 'Power') {
878
878
  const currentPowerTarget = mode.getSetting('power');
879
879
  const targetPower = Number(currentPowerTarget) + powerInc;
@@ -202,7 +202,6 @@ let FreeRideDisplayService = (() => {
202
202
  findPointBeforeOrAtTurn(path) {
203
203
  let prevPointIdx;
204
204
  let atPoint = false;
205
- let found = false;
206
205
  let idx = path.findIndex(p => p.routeDistance >= this.position.routeDistance);
207
206
  if (idx > 0) {
208
207
  const point = path[idx];
@@ -212,7 +211,7 @@ let FreeRideDisplayService = (() => {
212
211
  }
213
212
  prevPointIdx = idx;
214
213
  }
215
- found = idx !== -1;
214
+ const found = idx !== -1;
216
215
  return { prevPointIdx, atPoint, found };
217
216
  }
218
217
  getDisplayProperties(props) {
@@ -403,6 +403,11 @@ let RLVDisplayService = (() => {
403
403
  }
404
404
  this.emit('state-update');
405
405
  }
406
+ onConvertError(message, video = this.currentVideo) {
407
+ if (!video)
408
+ return;
409
+ this.onVideoLoadError({ message }, video);
410
+ }
406
411
  onVideoStalled(time, bufferedTime, buffers, video = this.currentVideo) {
407
412
  video.syncHelper.onVideoStalled(time, bufferedTime);
408
413
  }
@@ -509,25 +514,10 @@ let RLVDisplayService = (() => {
509
514
  video.playback = 'converted';
510
515
  video.source = new video_1.VideoConversion(this.cleanupUrl(src), video?.route);
511
516
  video.source.getObserver().on('convert-progress', this.onConvertUpdate.bind(this));
517
+ video.source.getObserver().on('convert-error', (message) => this.onConvertError(message, video));
512
518
  }
513
519
  cleanupUrl(url) {
514
- if (!url)
515
- return;
516
- let fileName = url;
517
- const lc = url.toLowerCase();
518
- if (fileName.startsWith('incyclist:'))
519
- fileName = fileName.replace('incyclist:', 'file:');
520
- if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
521
- return fileName.replace('video:', 'file:');
522
- if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !this.isMobile())
523
- return fileName.replace('file:', 'video:');
524
- if (fileName.startsWith('video:') && lc.endsWith('.avi'))
525
- return fileName;
526
- if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
527
- return fileName;
528
- if (fileName.startsWith('content:'))
529
- return fileName;
530
- return `./${fileName}`;
520
+ return (0, video_1.resolvePlaybackUrl)(url, this.isMobile());
531
521
  }
532
522
  isLoopEnabled() {
533
523
  return this.isLoop() && !this.startSettings?.loopOverwrite;
@@ -89,7 +89,7 @@ class IncyclistXMLParser extends xml_1.XMLParser {
89
89
  }
90
90
  catch (err) {
91
91
  if (!data['gpx-file-path'])
92
- throw new Error('<gpx-file-path> missing in XML');
92
+ throw new Error('<gpx-file-path> missing in XML', { cause: err });
93
93
  throw err;
94
94
  }
95
95
  }
@@ -244,7 +244,7 @@ class XMLParser {
244
244
  }
245
245
  catch (err) {
246
246
  this.getLogger().logEvent({ message: 'xml details', error: err.message, mappings });
247
- throw new Error('Could not parse XML File');
247
+ throw new Error('Could not parse XML File', { cause: err });
248
248
  }
249
249
  }
250
250
  }
@@ -57,7 +57,7 @@ const detectAnomalies = (points, maxCount) => {
57
57
  };
58
58
  const onSpeedJumpDetected = (point, idx) => {
59
59
  const prevPoint = data[lastValidSpeedIdx];
60
- let nextJumpIdx, nextNullIdx;
60
+ let nextNullIdx;
61
61
  if (prevPoint.speed === 0) {
62
62
  anomalies.push({ idx: lastValidSpeedIdx, type: 'noDistance', point });
63
63
  }
@@ -72,7 +72,7 @@ const detectAnomalies = (points, maxCount) => {
72
72
  continue;
73
73
  }
74
74
  }
75
- anomalies.push({ idx, type: 'speedJump', point, nextNullIdx, nextJumpIdx });
75
+ anomalies.push({ idx, type: 'speedJump', point, nextNullIdx });
76
76
  }
77
77
  };
78
78
  data.forEach((point, idx) => {
@@ -106,7 +106,7 @@ const fixAnomaly = (anomaly, points) => {
106
106
  exports.fixAnomaly = fixAnomaly;
107
107
  const fixAnomalies = (points, maxAttempts = 1000) => {
108
108
  let fixed = 0;
109
- let anomalies = [];
109
+ let anomalies;
110
110
  const tsStart = Date.now();
111
111
  let prev;
112
112
  let stuck = false;
@@ -83,7 +83,7 @@ let RoutesDbLoader = (() => {
83
83
  } };
84
84
  const updatedDescr = this.buildRouteDBInfo(route.description);
85
85
  const idx = this.routeDescriptions.findIndex(d => d.id === route.description.id);
86
- let changed = false;
86
+ let changed;
87
87
  if (idx === -1) {
88
88
  this.routeDescriptions.push((0, clone_1.default)(updatedDescr));
89
89
  this.routeHashes[route.description.id] = stringify(updatedDescr);
@@ -50,7 +50,9 @@ const monitoring_1 = require("../monitoring");
50
50
  const appstate_1 = require("../appstate");
51
51
  const pages_1 = require("../base/pages");
52
52
  const utils_1 = require("../utils");
53
+ const sleep_1 = require("../utils/sleep");
53
54
  const BACKGROUND_PAUSE_TIMEOUT_MS = 300000;
55
+ const APP_EXIT_TIMEOUT_MS = 5000;
54
56
  let UserInterfaceServcie = (() => {
55
57
  let _classDecorators = [decorators_1.Singleton];
56
58
  let _classDescriptor;
@@ -146,9 +148,12 @@ let UserInterfaceServcie = (() => {
146
148
  this.stopHeartbeatWorker();
147
149
  this.sendAppExitMessage();
148
150
  pages_1.IncyclistPageService.closePage();
149
- await (0, utils_1.waitNextTick)();
150
- await (0, devices_1.useDevicePairing)().exit();
151
- await (0, devices_1.useDeviceAccess)().terminate();
151
+ const cleanup = async () => {
152
+ await (0, utils_1.waitNextTick)();
153
+ await (0, devices_1.useDevicePairing)().exit();
154
+ await (0, devices_1.useDeviceAccess)().terminate();
155
+ };
156
+ await Promise.race([cleanup(), (0, sleep_1.sleep)(APP_EXIT_TIMEOUT_MS)]);
152
157
  this.appState = 'Stopped';
153
158
  this.logEvent({ message: 'onAppExit finished' });
154
159
  this.isTerminated = true;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseMp4Boxes = void 0;
4
+ const readBoxHeader = (buf, offset) => {
5
+ if (offset + 8 > buf.length)
6
+ return undefined;
7
+ const size32 = buf.readUInt32BE(offset);
8
+ const type = buf.toString('ascii', offset + 4, offset + 8);
9
+ let headerSize = 8;
10
+ let size = size32;
11
+ if (size32 === 1) {
12
+ if (offset + 16 > buf.length)
13
+ return undefined;
14
+ const high = buf.readUInt32BE(offset + 8);
15
+ const low = buf.readUInt32BE(offset + 12);
16
+ size = high * 2 ** 32 + low;
17
+ headerSize = 16;
18
+ }
19
+ else if (size32 === 0) {
20
+ size = buf.length - offset;
21
+ }
22
+ return { type, start: offset, size, headerSize, bodyStart: offset + headerSize, bodyEnd: offset + size };
23
+ };
24
+ const findBoxes = (buf, start, end) => {
25
+ const boxes = [];
26
+ let offset = start;
27
+ while (offset < end) {
28
+ const box = readBoxHeader(buf, offset);
29
+ if (!box || box.size <= 0 || box.bodyEnd > end)
30
+ break;
31
+ boxes.push(box);
32
+ offset = box.bodyEnd;
33
+ }
34
+ return boxes;
35
+ };
36
+ const findBox = (boxes, type) => boxes.find(b => b.type === type);
37
+ const findMoovBySignature = (buf) => {
38
+ const signature = Buffer.from('moov', 'ascii');
39
+ let searchFrom = 0;
40
+ while (searchFrom < buf.length) {
41
+ const typeOffset = buf.indexOf(signature, searchFrom);
42
+ if (typeOffset === -1 || typeOffset < 4)
43
+ return undefined;
44
+ const box = readBoxHeader(buf, typeOffset - 4);
45
+ if (box?.type === 'moov' && box.bodyEnd <= buf.length)
46
+ return box;
47
+ searchFrom = typeOffset + 4;
48
+ }
49
+ return undefined;
50
+ };
51
+ const parseFtyp = (buf, box) => {
52
+ if (box.bodyStart + 4 > buf.length)
53
+ return undefined;
54
+ return buf.toString('ascii', box.bodyStart, box.bodyStart + 4);
55
+ };
56
+ const parseHandlerType = (buf, hdlrBox) => {
57
+ const offset = hdlrBox.bodyStart + 8;
58
+ if (offset + 4 > buf.length)
59
+ return undefined;
60
+ return buf.toString('ascii', offset, offset + 4);
61
+ };
62
+ const parseStsdCodec = (buf, stsdBox) => {
63
+ const entryOffset = stsdBox.bodyStart + 8;
64
+ if (entryOffset + 8 > buf.length)
65
+ return undefined;
66
+ return buf.toString('ascii', entryOffset + 4, entryOffset + 8);
67
+ };
68
+ const parseTkhdDimensions = (buf, tkhdBox) => {
69
+ if (tkhdBox.bodyStart >= buf.length)
70
+ return {};
71
+ const version = buf.readUInt8(tkhdBox.bodyStart);
72
+ const fixedFieldsSize = version === 1 ? 96 : 76;
73
+ const offset = tkhdBox.bodyStart + fixedFieldsSize;
74
+ if (offset + 8 > buf.length)
75
+ return {};
76
+ const width = buf.readUInt32BE(offset) >> 16;
77
+ const height = buf.readUInt32BE(offset + 4) >> 16;
78
+ return { width, height };
79
+ };
80
+ const parseTrak = (buf, trakBox) => {
81
+ const trakChildren = findBoxes(buf, trakBox.bodyStart, trakBox.bodyEnd);
82
+ const tkhdBox = findBox(trakChildren, 'tkhd');
83
+ const mdiaBox = findBox(trakChildren, 'mdia');
84
+ if (!mdiaBox)
85
+ return {};
86
+ const mdiaChildren = findBoxes(buf, mdiaBox.bodyStart, mdiaBox.bodyEnd);
87
+ const hdlrBox = findBox(mdiaChildren, 'hdlr');
88
+ const minfBox = findBox(mdiaChildren, 'minf');
89
+ const handlerType = hdlrBox ? parseHandlerType(buf, hdlrBox) : undefined;
90
+ let codec;
91
+ if (minfBox) {
92
+ const minfChildren = findBoxes(buf, minfBox.bodyStart, minfBox.bodyEnd);
93
+ const stblBox = findBox(minfChildren, 'stbl');
94
+ if (stblBox) {
95
+ const stblChildren = findBoxes(buf, stblBox.bodyStart, stblBox.bodyEnd);
96
+ const stsdBox = findBox(stblChildren, 'stsd');
97
+ if (stsdBox)
98
+ codec = parseStsdCodec(buf, stsdBox);
99
+ }
100
+ }
101
+ const dimensions = (handlerType === 'vide' && tkhdBox) ? parseTkhdDimensions(buf, tkhdBox) : {};
102
+ return { handlerType, codec, ...dimensions };
103
+ };
104
+ const parseMp4Boxes = (headBuffer, tailBuffer) => {
105
+ const result = { moovLocation: 'not-found', incomplete: true };
106
+ if (headBuffer?.length > 0) {
107
+ const headBoxes = findBoxes(headBuffer, 0, headBuffer.length);
108
+ const ftypBox = findBox(headBoxes, 'ftyp');
109
+ if (ftypBox)
110
+ result.containerBrand = parseFtyp(headBuffer, ftypBox);
111
+ const moovBox = findBox(headBoxes, 'moov');
112
+ if (moovBox) {
113
+ applyMoov(headBuffer, moovBox, result);
114
+ result.moovLocation = 'head';
115
+ result.incomplete = false;
116
+ }
117
+ }
118
+ if (result.moovLocation === 'not-found' && tailBuffer?.length > 0) {
119
+ const moovBox = findMoovBySignature(tailBuffer);
120
+ if (moovBox) {
121
+ applyMoov(tailBuffer, moovBox, result);
122
+ result.moovLocation = 'tail';
123
+ result.incomplete = false;
124
+ }
125
+ }
126
+ return result;
127
+ };
128
+ exports.parseMp4Boxes = parseMp4Boxes;
129
+ const applyMoov = (buf, moovBox, result) => {
130
+ const moovChildren = findBoxes(buf, moovBox.bodyStart, moovBox.bodyEnd);
131
+ const trakBoxes = moovChildren.filter(b => b.type === 'trak');
132
+ trakBoxes.forEach(trakBox => {
133
+ const { handlerType, codec, width, height } = parseTrak(buf, trakBox);
134
+ if (handlerType === 'vide') {
135
+ result.videoCodec = codec;
136
+ if (width)
137
+ result.width = width;
138
+ if (height)
139
+ result.height = height;
140
+ }
141
+ else if (handlerType === 'soun') {
142
+ result.audioCodec = codec;
143
+ }
144
+ });
145
+ };
@@ -115,7 +115,7 @@ let VideoConversion = (() => {
115
115
  return undefined;
116
116
  const clean = str.trim();
117
117
  const parts = clean.split(':');
118
- let seconds = 0;
118
+ let seconds;
119
119
  if (parts.length === 3) {
120
120
  const [h, m, s] = parts;
121
121
  seconds = Number(h) * 3600 + Number(m) * 60 + Number.parseFloat(s);
@@ -17,3 +17,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./VideoSyncHelper"), exports);
19
19
  __exportStar(require("./VideoConversion"), exports);
20
+ __exportStar(require("./Mp4BoxParser"), exports);
21
+ __exportStar(require("./resolvePlaybackUrl"), exports);
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvePlaybackUrl = void 0;
4
+ const resolvePlaybackUrl = (url, isMobile = false) => {
5
+ if (!url)
6
+ return undefined;
7
+ let fileName = url;
8
+ const lc = url.toLowerCase();
9
+ if (fileName.startsWith('incyclist:'))
10
+ fileName = fileName.replace('incyclist:', 'file:');
11
+ if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
12
+ return fileName.replace('video:', 'file:');
13
+ if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !isMobile)
14
+ return fileName.replace('file:', 'video:');
15
+ if (fileName.startsWith('video:') && lc.endsWith('.avi'))
16
+ return fileName;
17
+ if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
18
+ return fileName;
19
+ if (fileName.startsWith('content:'))
20
+ return fileName;
21
+ return `./${fileName}`;
22
+ };
23
+ exports.resolvePlaybackUrl = resolvePlaybackUrl;
@@ -203,7 +203,7 @@ let WorkoutCard = (() => {
203
203
  }
204
204
  async _delete() {
205
205
  await (0, utils_1.waitNextTick)();
206
- let deleted = false;
206
+ let deleted;
207
207
  try {
208
208
  this.deleteObserver.emit('started');
209
209
  await this.getRepo().delete(this.workout);
@@ -97,7 +97,7 @@ let WorkoutsDbLoader = (() => {
97
97
  };
98
98
  let prev;
99
99
  const idx = this.workouts.findIndex(d => d.id === workout.id);
100
- let changed = false;
100
+ let changed;
101
101
  if (idx === -1) {
102
102
  changed = true;
103
103
  this.workouts.push(workout);
@@ -57,14 +57,14 @@ let IncyclistFitConvertApi = (() => {
57
57
  url = response.data;
58
58
  }
59
59
  catch (error) {
60
- throw new Error(`convert failed: (phase convert), reason: ${error.message}`);
60
+ throw new Error(`convert failed: (phase convert), reason: ${error.message}`, { cause: error });
61
61
  }
62
62
  try {
63
63
  const response = await this.getApi().get(url, { responseType: 'arraybuffer' });
64
64
  return response.data;
65
65
  }
66
66
  catch (error) {
67
- throw new Error(`convert failed: (phase download), reason: ${error.message}`);
67
+ throw new Error(`convert failed: (phase download), reason: ${error.message}`, { cause: error });
68
68
  }
69
69
  }
70
70
  getApi() {
@@ -262,8 +262,8 @@ let ActivitiesRepository = (() => {
262
262
  if (typeof endPos !== 'number' && endPos?.value !== undefined) {
263
263
  endPos = this.getUnitConverter().convert(endPos.value, 'distance', { from: endPos.unit, to: 'm' });
264
264
  }
265
- if (result?.length > 0 && criteria?.endPos !== undefined) {
266
- result = result.filter(ai => ai.summary.endPos === criteria.endPos);
265
+ if (result?.length > 0 && endPos !== undefined) {
266
+ result = result.filter(ai => ai.summary.endPos === endPos);
267
267
  }
268
268
  return result;
269
269
  }
@@ -233,6 +233,7 @@ let Activity = (() => {
233
233
  }
234
234
  catch (err) {
235
235
  this.logError(err, 'upload');
236
+ error = err.message;
236
237
  }
237
238
  }
238
239
  this.markUploading(connectedApp, false);
@@ -35,7 +35,7 @@ export class VeloHeroApi extends AppApiBase {
35
35
  const { response } = err;
36
36
  error = `HTTP error ${response.status}: ${response.statusText}`;
37
37
  }
38
- throw new Error(error);
38
+ throw new Error(error, { cause: err });
39
39
  }
40
40
  }
41
41
  getAccountType() {
@@ -119,7 +119,7 @@ let IntervalsAppConnection = (() => {
119
119
  return this.api;
120
120
  }
121
121
  initAuth() {
122
- let isInitialized = false;
122
+ let isInitialized;
123
123
  try {
124
124
  const auth = this.getCredentials();
125
125
  if (auth) {
@@ -116,7 +116,7 @@ let KomootAppConnection = (() => {
116
116
  return false;
117
117
  }
118
118
  checkAuth() {
119
- let isInitialized = false;
119
+ let isInitialized;
120
120
  let credentials;
121
121
  if (!this.getUserSettings().isInitialized) {
122
122
  return false;
@@ -117,7 +117,7 @@ let StravaAppConnection = (() => {
117
117
  return this.api;
118
118
  }
119
119
  initAuth() {
120
- let isInitialized = false;
120
+ let isInitialized;
121
121
  try {
122
122
  const auth = this.getCredentials();
123
123
  const clientId = this.getSecret('STRAVA_CLIENT_ID');
@@ -102,7 +102,7 @@ let VeloHeroAppConnection = (() => {
102
102
  return this.api;
103
103
  }
104
104
  initAuth() {
105
- let isInitialized = false;
105
+ let isInitialized;
106
106
  if (!this.getUserSettings().isInitialized) {
107
107
  return false;
108
108
  }
@@ -39,6 +39,7 @@ import { IncyclistService } from "../base/service";
39
39
  import { Injectable } from "../base/decorators";
40
40
  const UPDATE_SERVER_URL = 'UPDATE_SERVER_URL';
41
41
  const DEFAULT_DOMAIN = 'incyclist.com';
42
+ const UPDATE_CHECK_TIMEOUT_MS = 5000;
42
43
  let IncyclistUpdatesApi = (() => {
43
44
  let _classDecorators = [Singleton];
44
45
  let _classDescriptor;
@@ -93,10 +94,19 @@ let IncyclistUpdatesApi = (() => {
93
94
  const base = this.getSetting(UPDATE_SERVER_URL, defaultUrl);
94
95
  return base;
95
96
  }
96
- async _get(url, ...args) {
97
+ async _get(url, config = {}) {
97
98
  const api = this.getApi();
98
99
  const baseUrl = this.getBaseUrl();
99
- return await api.get(baseUrl + url, ...args);
100
+ return await api.get(baseUrl + url, { timeout: UPDATE_CHECK_TIMEOUT_MS, ...config });
101
+ }
102
+ isExpectedError(err) {
103
+ if (!err)
104
+ return false;
105
+ if (err.message === 'Network Error')
106
+ return true;
107
+ if (/timeout/i.test(err.message ?? ''))
108
+ return true;
109
+ return false;
100
110
  }
101
111
  async getLatestMacAppVersion() {
102
112
  try {
@@ -111,7 +121,7 @@ let IncyclistUpdatesApi = (() => {
111
121
  return { version, path, url, downloadUrl };
112
122
  }
113
123
  catch (err) {
114
- if (err.message !== 'Network Error') {
124
+ if (!this.isExpectedError(err)) {
115
125
  this.logError(err, 'getLatestMacAppVersion');
116
126
  }
117
127
  }
@@ -139,7 +149,7 @@ let IncyclistUpdatesApi = (() => {
139
149
  return { version, path, url, downloadUrl };
140
150
  }
141
151
  catch (err) {
142
- if (err.message !== 'Network Error') {
152
+ if (!this.isExpectedError(err)) {
143
153
  this.logError(err, 'getLatestLinuxAppVersion');
144
154
  }
145
155
  }
@@ -162,7 +172,7 @@ let IncyclistUpdatesApi = (() => {
162
172
  return { version, path, url, downloadUrl };
163
173
  }
164
174
  catch (err) {
165
- if (err.message !== 'Network Error') {
175
+ if (!this.isExpectedError(err)) {
166
176
  this.logError(err, 'getLatestWindowsAppVersion');
167
177
  }
168
178
  }
@@ -254,9 +254,8 @@ let CoachesService = (() => {
254
254
  if (!route)
255
255
  return;
256
256
  const prevPosition = coach.getPosition() || route.points[0];
257
- let nextPosition = undefined;
258
257
  if (distance > 0) {
259
- nextPosition = getNextPosition(route, { distance, prev: prevPosition });
258
+ const nextPosition = getNextPosition(route, { distance, prev: prevPosition });
260
259
  if (nextPosition) {
261
260
  const { slope } = nextPosition;
262
261
  coach.sendDeviceUpdate({ slope: slope * realityFactor / 100 });
@@ -1128,7 +1128,7 @@ let DeviceRideService = (() => {
1128
1128
  const adapters = this.getRideAdapters();
1129
1129
  const config = this.getDeviceConfiguration();
1130
1130
  const selectedDevices = selected ?? config.getSelectedDevices();
1131
- let enabledCapabilities = [];
1131
+ let enabledCapabilities;
1132
1132
  const duplicates = this.checkAntSameDeviceID(adapters);
1133
1133
  const toBeReplaced = duplicates.filter(dai => dai.udid === adapterInfo.udid).map(dai => dai.info.udid);
1134
1134
  if (this.simulatorEnforced) {
@@ -140,7 +140,6 @@ export class MapArea {
140
140
  };
141
141
  const addOption = (newPath, res, reverse) => {
142
142
  const optWay = way;
143
- let path = [];
144
143
  if (newPath.length === 1) {
145
144
  addWaysCrossing(res);
146
145
  }
@@ -150,7 +149,7 @@ export class MapArea {
150
149
  newPath[0].id === way.path.at(-1).id)) {
151
150
  addWaysCrossing(res);
152
151
  }
153
- path = newPath;
152
+ const path = newPath;
154
153
  if (reverse)
155
154
  path.reverse();
156
155
  res.push({ wayId: optWay.id, path });
@@ -167,6 +167,7 @@ let MapAreaService = (() => {
167
167
  let ts;
168
168
  for (let i = 0; i < 2; i++) {
169
169
  try {
170
+ ts = Date.now();
170
171
  const bounds = getBounds(location?.lat, location?.lng, this.radius);
171
172
  const query = buildQuery(GET_WAYS_IN_AREA, bounds);
172
173
  const openmapData = await this.getOverpassAPI().query(query, DEFAULT_TIMEOUT);
@@ -870,7 +870,7 @@ let RideDisplayService = (() => {
870
870
  }
871
871
  simulatorPowerUp(mode, powerInc) {
872
872
  const simMode = mode.getSetting('mode');
873
- let request = {};
873
+ let request;
874
874
  if (simMode === 'Power') {
875
875
  const currentPowerTarget = mode.getSetting('power');
876
876
  const targetPower = Number(currentPowerTarget) + powerInc;
@@ -196,7 +196,6 @@ let FreeRideDisplayService = (() => {
196
196
  findPointBeforeOrAtTurn(path) {
197
197
  let prevPointIdx;
198
198
  let atPoint = false;
199
- let found = false;
200
199
  let idx = path.findIndex(p => p.routeDistance >= this.position.routeDistance);
201
200
  if (idx > 0) {
202
201
  const point = path[idx];
@@ -206,7 +205,7 @@ let FreeRideDisplayService = (() => {
206
205
  }
207
206
  prevPointIdx = idx;
208
207
  }
209
- found = idx !== -1;
208
+ const found = idx !== -1;
210
209
  return { prevPointIdx, atPoint, found };
211
210
  }
212
211
  getDisplayProperties(props) {
@@ -40,7 +40,7 @@ import { useLocalization } from "../../i18n";
40
40
  import { concatPaths } from "../../maps/MapArea/utils";
41
41
  import { correctDistanceValues, getNextVideoId, hasNextVideo, useRouteList, validateRoute } from "../../routes";
42
42
  import { distanceBetween } from "../../utils/geo";
43
- import { VideoConversion, VideoSyncHelper } from "../../video";
43
+ import { VideoConversion, VideoSyncHelper, resolvePlaybackUrl } from "../../video";
44
44
  import { RouteDisplayService } from "./RouteDisplayService";
45
45
  let RLVDisplayService = (() => {
46
46
  let _classSuper = RouteDisplayService;
@@ -400,6 +400,11 @@ let RLVDisplayService = (() => {
400
400
  }
401
401
  this.emit('state-update');
402
402
  }
403
+ onConvertError(message, video = this.currentVideo) {
404
+ if (!video)
405
+ return;
406
+ this.onVideoLoadError({ message }, video);
407
+ }
403
408
  onVideoStalled(time, bufferedTime, buffers, video = this.currentVideo) {
404
409
  video.syncHelper.onVideoStalled(time, bufferedTime);
405
410
  }
@@ -506,25 +511,10 @@ let RLVDisplayService = (() => {
506
511
  video.playback = 'converted';
507
512
  video.source = new VideoConversion(this.cleanupUrl(src), video?.route);
508
513
  video.source.getObserver().on('convert-progress', this.onConvertUpdate.bind(this));
514
+ video.source.getObserver().on('convert-error', (message) => this.onConvertError(message, video));
509
515
  }
510
516
  cleanupUrl(url) {
511
- if (!url)
512
- return;
513
- let fileName = url;
514
- const lc = url.toLowerCase();
515
- if (fileName.startsWith('incyclist:'))
516
- fileName = fileName.replace('incyclist:', 'file:');
517
- if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
518
- return fileName.replace('video:', 'file:');
519
- if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !this.isMobile())
520
- return fileName.replace('file:', 'video:');
521
- if (fileName.startsWith('video:') && lc.endsWith('.avi'))
522
- return fileName;
523
- if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
524
- return fileName;
525
- if (fileName.startsWith('content:'))
526
- return fileName;
527
- return `./${fileName}`;
517
+ return resolvePlaybackUrl(url, this.isMobile());
528
518
  }
529
519
  isLoopEnabled() {
530
520
  return this.isLoop() && !this.startSettings?.loopOverwrite;
@@ -86,7 +86,7 @@ export class IncyclistXMLParser extends XMLParser {
86
86
  }
87
87
  catch (err) {
88
88
  if (!data['gpx-file-path'])
89
- throw new Error('<gpx-file-path> missing in XML');
89
+ throw new Error('<gpx-file-path> missing in XML', { cause: err });
90
90
  throw err;
91
91
  }
92
92
  }
@@ -241,7 +241,7 @@ export class XMLParser {
241
241
  }
242
242
  catch (err) {
243
243
  this.getLogger().logEvent({ message: 'xml details', error: err.message, mappings });
244
- throw new Error('Could not parse XML File');
244
+ throw new Error('Could not parse XML File', { cause: err });
245
245
  }
246
246
  }
247
247
  }
@@ -52,7 +52,7 @@ export const detectAnomalies = (points, maxCount) => {
52
52
  };
53
53
  const onSpeedJumpDetected = (point, idx) => {
54
54
  const prevPoint = data[lastValidSpeedIdx];
55
- let nextJumpIdx, nextNullIdx;
55
+ let nextNullIdx;
56
56
  if (prevPoint.speed === 0) {
57
57
  anomalies.push({ idx: lastValidSpeedIdx, type: 'noDistance', point });
58
58
  }
@@ -67,7 +67,7 @@ export const detectAnomalies = (points, maxCount) => {
67
67
  continue;
68
68
  }
69
69
  }
70
- anomalies.push({ idx, type: 'speedJump', point, nextNullIdx, nextJumpIdx });
70
+ anomalies.push({ idx, type: 'speedJump', point, nextNullIdx });
71
71
  }
72
72
  };
73
73
  data.forEach((point, idx) => {
@@ -99,7 +99,7 @@ export const fixAnomaly = (anomaly, points) => {
99
99
  };
100
100
  export const fixAnomalies = (points, maxAttempts = 1000) => {
101
101
  let fixed = 0;
102
- let anomalies = [];
102
+ let anomalies;
103
103
  const tsStart = Date.now();
104
104
  let prev;
105
105
  let stuck = false;
@@ -77,7 +77,7 @@ let RoutesDbLoader = (() => {
77
77
  } };
78
78
  const updatedDescr = this.buildRouteDBInfo(route.description);
79
79
  const idx = this.routeDescriptions.findIndex(d => d.id === route.description.id);
80
- let changed = false;
80
+ let changed;
81
81
  if (idx === -1) {
82
82
  this.routeDescriptions.push(clone(updatedDescr));
83
83
  this.routeHashes[route.description.id] = stringify(updatedDescr);
@@ -47,7 +47,9 @@ import { useOnlineStatusMonitoring } from "../monitoring";
47
47
  import { useAppState, useFeatureToggleSync } from "../appstate";
48
48
  import { IncyclistPageService } from "../base/pages";
49
49
  import { waitNextTick } from "../utils";
50
+ import { sleep } from "../utils/sleep";
50
51
  const BACKGROUND_PAUSE_TIMEOUT_MS = 300000;
52
+ const APP_EXIT_TIMEOUT_MS = 5000;
51
53
  let UserInterfaceServcie = (() => {
52
54
  let _classDecorators = [Singleton];
53
55
  let _classDescriptor;
@@ -143,9 +145,12 @@ let UserInterfaceServcie = (() => {
143
145
  this.stopHeartbeatWorker();
144
146
  this.sendAppExitMessage();
145
147
  IncyclistPageService.closePage();
146
- await waitNextTick();
147
- await useDevicePairing().exit();
148
- await useDeviceAccess().terminate();
148
+ const cleanup = async () => {
149
+ await waitNextTick();
150
+ await useDevicePairing().exit();
151
+ await useDeviceAccess().terminate();
152
+ };
153
+ await Promise.race([cleanup(), sleep(APP_EXIT_TIMEOUT_MS)]);
149
154
  this.appState = 'Stopped';
150
155
  this.logEvent({ message: 'onAppExit finished' });
151
156
  this.isTerminated = true;
@@ -0,0 +1,141 @@
1
+ const readBoxHeader = (buf, offset) => {
2
+ if (offset + 8 > buf.length)
3
+ return undefined;
4
+ const size32 = buf.readUInt32BE(offset);
5
+ const type = buf.toString('ascii', offset + 4, offset + 8);
6
+ let headerSize = 8;
7
+ let size = size32;
8
+ if (size32 === 1) {
9
+ if (offset + 16 > buf.length)
10
+ return undefined;
11
+ const high = buf.readUInt32BE(offset + 8);
12
+ const low = buf.readUInt32BE(offset + 12);
13
+ size = high * 2 ** 32 + low;
14
+ headerSize = 16;
15
+ }
16
+ else if (size32 === 0) {
17
+ size = buf.length - offset;
18
+ }
19
+ return { type, start: offset, size, headerSize, bodyStart: offset + headerSize, bodyEnd: offset + size };
20
+ };
21
+ const findBoxes = (buf, start, end) => {
22
+ const boxes = [];
23
+ let offset = start;
24
+ while (offset < end) {
25
+ const box = readBoxHeader(buf, offset);
26
+ if (!box || box.size <= 0 || box.bodyEnd > end)
27
+ break;
28
+ boxes.push(box);
29
+ offset = box.bodyEnd;
30
+ }
31
+ return boxes;
32
+ };
33
+ const findBox = (boxes, type) => boxes.find(b => b.type === type);
34
+ const findMoovBySignature = (buf) => {
35
+ const signature = Buffer.from('moov', 'ascii');
36
+ let searchFrom = 0;
37
+ while (searchFrom < buf.length) {
38
+ const typeOffset = buf.indexOf(signature, searchFrom);
39
+ if (typeOffset === -1 || typeOffset < 4)
40
+ return undefined;
41
+ const box = readBoxHeader(buf, typeOffset - 4);
42
+ if (box?.type === 'moov' && box.bodyEnd <= buf.length)
43
+ return box;
44
+ searchFrom = typeOffset + 4;
45
+ }
46
+ return undefined;
47
+ };
48
+ const parseFtyp = (buf, box) => {
49
+ if (box.bodyStart + 4 > buf.length)
50
+ return undefined;
51
+ return buf.toString('ascii', box.bodyStart, box.bodyStart + 4);
52
+ };
53
+ const parseHandlerType = (buf, hdlrBox) => {
54
+ const offset = hdlrBox.bodyStart + 8;
55
+ if (offset + 4 > buf.length)
56
+ return undefined;
57
+ return buf.toString('ascii', offset, offset + 4);
58
+ };
59
+ const parseStsdCodec = (buf, stsdBox) => {
60
+ const entryOffset = stsdBox.bodyStart + 8;
61
+ if (entryOffset + 8 > buf.length)
62
+ return undefined;
63
+ return buf.toString('ascii', entryOffset + 4, entryOffset + 8);
64
+ };
65
+ const parseTkhdDimensions = (buf, tkhdBox) => {
66
+ if (tkhdBox.bodyStart >= buf.length)
67
+ return {};
68
+ const version = buf.readUInt8(tkhdBox.bodyStart);
69
+ const fixedFieldsSize = version === 1 ? 96 : 76;
70
+ const offset = tkhdBox.bodyStart + fixedFieldsSize;
71
+ if (offset + 8 > buf.length)
72
+ return {};
73
+ const width = buf.readUInt32BE(offset) >> 16;
74
+ const height = buf.readUInt32BE(offset + 4) >> 16;
75
+ return { width, height };
76
+ };
77
+ const parseTrak = (buf, trakBox) => {
78
+ const trakChildren = findBoxes(buf, trakBox.bodyStart, trakBox.bodyEnd);
79
+ const tkhdBox = findBox(trakChildren, 'tkhd');
80
+ const mdiaBox = findBox(trakChildren, 'mdia');
81
+ if (!mdiaBox)
82
+ return {};
83
+ const mdiaChildren = findBoxes(buf, mdiaBox.bodyStart, mdiaBox.bodyEnd);
84
+ const hdlrBox = findBox(mdiaChildren, 'hdlr');
85
+ const minfBox = findBox(mdiaChildren, 'minf');
86
+ const handlerType = hdlrBox ? parseHandlerType(buf, hdlrBox) : undefined;
87
+ let codec;
88
+ if (minfBox) {
89
+ const minfChildren = findBoxes(buf, minfBox.bodyStart, minfBox.bodyEnd);
90
+ const stblBox = findBox(minfChildren, 'stbl');
91
+ if (stblBox) {
92
+ const stblChildren = findBoxes(buf, stblBox.bodyStart, stblBox.bodyEnd);
93
+ const stsdBox = findBox(stblChildren, 'stsd');
94
+ if (stsdBox)
95
+ codec = parseStsdCodec(buf, stsdBox);
96
+ }
97
+ }
98
+ const dimensions = (handlerType === 'vide' && tkhdBox) ? parseTkhdDimensions(buf, tkhdBox) : {};
99
+ return { handlerType, codec, ...dimensions };
100
+ };
101
+ export const parseMp4Boxes = (headBuffer, tailBuffer) => {
102
+ const result = { moovLocation: 'not-found', incomplete: true };
103
+ if (headBuffer?.length > 0) {
104
+ const headBoxes = findBoxes(headBuffer, 0, headBuffer.length);
105
+ const ftypBox = findBox(headBoxes, 'ftyp');
106
+ if (ftypBox)
107
+ result.containerBrand = parseFtyp(headBuffer, ftypBox);
108
+ const moovBox = findBox(headBoxes, 'moov');
109
+ if (moovBox) {
110
+ applyMoov(headBuffer, moovBox, result);
111
+ result.moovLocation = 'head';
112
+ result.incomplete = false;
113
+ }
114
+ }
115
+ if (result.moovLocation === 'not-found' && tailBuffer?.length > 0) {
116
+ const moovBox = findMoovBySignature(tailBuffer);
117
+ if (moovBox) {
118
+ applyMoov(tailBuffer, moovBox, result);
119
+ result.moovLocation = 'tail';
120
+ result.incomplete = false;
121
+ }
122
+ }
123
+ return result;
124
+ };
125
+ const applyMoov = (buf, moovBox, result) => {
126
+ const moovChildren = findBoxes(buf, moovBox.bodyStart, moovBox.bodyEnd);
127
+ const trakBoxes = moovChildren.filter(b => b.type === 'trak');
128
+ trakBoxes.forEach(trakBox => {
129
+ const { handlerType, codec, width, height } = parseTrak(buf, trakBox);
130
+ if (handlerType === 'vide') {
131
+ result.videoCodec = codec;
132
+ if (width)
133
+ result.width = width;
134
+ if (height)
135
+ result.height = height;
136
+ }
137
+ else if (handlerType === 'soun') {
138
+ result.audioCodec = codec;
139
+ }
140
+ });
141
+ };
@@ -112,7 +112,7 @@ let VideoConversion = (() => {
112
112
  return undefined;
113
113
  const clean = str.trim();
114
114
  const parts = clean.split(':');
115
- let seconds = 0;
115
+ let seconds;
116
116
  if (parts.length === 3) {
117
117
  const [h, m, s] = parts;
118
118
  seconds = Number(h) * 3600 + Number(m) * 60 + Number.parseFloat(s);
@@ -1,3 +1,5 @@
1
1
  export * from './types';
2
2
  export * from './VideoSyncHelper';
3
3
  export * from './VideoConversion';
4
+ export * from './Mp4BoxParser';
5
+ export * from './resolvePlaybackUrl';
@@ -0,0 +1,19 @@
1
+ export const resolvePlaybackUrl = (url, isMobile = false) => {
2
+ if (!url)
3
+ return undefined;
4
+ let fileName = url;
5
+ const lc = url.toLowerCase();
6
+ if (fileName.startsWith('incyclist:'))
7
+ fileName = fileName.replace('incyclist:', 'file:');
8
+ if (fileName.startsWith('video:') && !lc.endsWith('.avi'))
9
+ return fileName.replace('video:', 'file:');
10
+ if (fileName.startsWith('file:') && !lc.endsWith('.avi') && !isMobile)
11
+ return fileName.replace('file:', 'video:');
12
+ if (fileName.startsWith('video:') && lc.endsWith('.avi'))
13
+ return fileName;
14
+ if (fileName.startsWith('file:') || fileName.startsWith('http:') || fileName.startsWith('https:') || fileName.startsWith('/'))
15
+ return fileName;
16
+ if (fileName.startsWith('content:'))
17
+ return fileName;
18
+ return `./${fileName}`;
19
+ };
@@ -200,7 +200,7 @@ let WorkoutCard = (() => {
200
200
  }
201
201
  async _delete() {
202
202
  await waitNextTick();
203
- let deleted = false;
203
+ let deleted;
204
204
  try {
205
205
  this.deleteObserver.emit('started');
206
206
  await this.getRepo().delete(this.workout);
@@ -94,7 +94,7 @@ let WorkoutsDbLoader = (() => {
94
94
  };
95
95
  let prev;
96
96
  const idx = this.workouts.findIndex(d => d.id === workout.id);
97
- let changed = false;
97
+ let changed;
98
98
  if (idx === -1) {
99
99
  changed = true;
100
100
  this.workouts.push(workout);
@@ -12,7 +12,8 @@ export declare class IncyclistUpdatesApi extends IncyclistService {
12
12
  getLatestAppVersion(platform: string): Promise<VersionInfo>;
13
13
  protected getApi(): AxiosInstance;
14
14
  protected getBaseUrl(): string;
15
- protected _get(url: string, ...args: any[]): Promise<import("axios").AxiosResponse<any, any, {}>>;
15
+ protected _get(url: string, config?: Record<string, any>): Promise<import("axios").AxiosResponse<any, any, {}>>;
16
+ protected isExpectedError(err: Error): boolean;
16
17
  protected getLatestMacAppVersion(): Promise<VersionInfo>;
17
18
  protected getSetting(key: string, def: any): any;
18
19
  protected getLatestLinuxAppVersion(): Promise<VersionInfo>;
@@ -69,6 +69,7 @@ export declare class RLVDisplayService extends RouteDisplayService {
69
69
  protected onConvertUpdate(progress: number, frames: number, time: number): void;
70
70
  protected onVideoLoaded(bufferedTime: number, video?: VideoState): void;
71
71
  protected onVideoLoadError(error: MediaError, video?: VideoState): void;
72
+ protected onConvertError(message: string, video?: VideoState): void;
72
73
  protected onVideoStalled(time: number, bufferedTime: number, buffers: Array<{
73
74
  start: number;
74
75
  end: number;
@@ -0,0 +1,10 @@
1
+ export type Mp4CodecInfo = {
2
+ containerBrand?: string;
3
+ videoCodec?: string;
4
+ audioCodec?: string;
5
+ width?: number;
6
+ height?: number;
7
+ moovLocation?: 'head' | 'tail' | 'not-found';
8
+ incomplete?: boolean;
9
+ };
10
+ export declare const parseMp4Boxes: (headBuffer?: Buffer, tailBuffer?: Buffer) => Mp4CodecInfo;
@@ -1,3 +1,5 @@
1
1
  export * from './types';
2
2
  export * from './VideoSyncHelper';
3
3
  export * from './VideoConversion';
4
+ export * from './Mp4BoxParser';
5
+ export * from './resolvePlaybackUrl';
@@ -0,0 +1 @@
1
+ export declare const resolvePlaybackUrl: (url?: string, isMobile?: boolean) => string | undefined;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.90",
3
+ "version": "1.7.92",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },
7
7
  "dependencies": {
8
8
  "@garmin/fitsdk": "^21.200.0",
9
9
  "axios": "^1.15.2",
10
- "incyclist-devices": "^3.0.26",
10
+ "incyclist-devices": "^3.0.29",
11
11
  "promise.any": "^2.0.6",
12
12
  "semver": "^7.7.4",
13
13
  "tcx-builder": "^1.1.1",
@@ -15,15 +15,17 @@
15
15
  "xml2js": "^0.6.2"
16
16
  },
17
17
  "devDependencies": {
18
+ "@eslint/js": "^10.0.1",
18
19
  "@serialport/binding-mock": "^10.2.2",
19
20
  "@serialport/bindings-interface": "^1.2.2",
20
21
  "@types/jest": "^30.0.0",
21
22
  "@types/node": "^25.6.0",
22
- "@typescript-eslint/eslint-plugin": "^8.58.2",
23
- "@typescript-eslint/parser": "^8.58.2",
23
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
24
+ "@typescript-eslint/parser": "^8.59.0",
24
25
  "dotenv": "^17.4.2",
25
- "eslint": "^9.39.4",
26
+ "eslint": "^10.2.1",
26
27
  "formdata-node": "^6.0.3",
28
+ "globals": "^17.9.0",
27
29
  "jest": "^30.3.0",
28
30
  "jsdoc": "^4.0.5",
29
31
  "ts-jest": "^29.4.9",
@@ -31,7 +33,7 @@
31
33
  "typedoc-plugin-markdown": "^4.11.0",
32
34
  "typedoc-plugin-no-inherit": "^1.6.1",
33
35
  "typescript": "^5.9.3",
34
- "typescript-eslint": "^8.58.2"
36
+ "typescript-eslint": "^8.59.0"
35
37
  },
36
38
  "scripts": {
37
39
  "lint": "eslint . --quiet",