incyclist-services 1.7.91 → 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.
- package/lib/cjs/activities/base/api/fitconvert/fitconvert.js +2 -2
- package/lib/cjs/activities/base/repo/db.js +2 -2
- package/lib/cjs/activities/base/utils/activity.js +1 -0
- package/lib/cjs/apps/base/api/velohero/api.js +1 -1
- package/lib/cjs/apps/intervals/IntervalsAppConnection.js +1 -1
- package/lib/cjs/apps/komoot/KomootAppConnection.js +1 -1
- package/lib/cjs/apps/strava/StravaAppConnection.js +1 -1
- package/lib/cjs/apps/velohero/VeloHeroAppConnection.js +1 -1
- package/lib/cjs/autoupdate/api.js +15 -5
- package/lib/cjs/coaches/service.js +1 -2
- package/lib/cjs/devices/ride/service.js +1 -1
- package/lib/cjs/maps/MapArea/MapArea.js +1 -2
- package/lib/cjs/maps/MapArea/service.js +1 -0
- package/lib/cjs/ride/display/service.js +1 -1
- package/lib/cjs/ride/route/FreeRideDisplayService.js +1 -2
- package/lib/cjs/ride/route/RLVDisplayService.js +7 -17
- package/lib/cjs/routes/base/parsers/incyclist.js +1 -1
- package/lib/cjs/routes/base/parsers/xml.js +1 -1
- package/lib/cjs/routes/base/utils/points.js +3 -3
- package/lib/cjs/routes/list/loaders/db.js +1 -1
- package/lib/cjs/ui/service.js +8 -3
- package/lib/cjs/video/Mp4BoxParser.js +145 -0
- package/lib/cjs/video/VideoConversion.js +1 -1
- package/lib/cjs/video/index.js +2 -0
- package/lib/cjs/video/resolvePlaybackUrl.js +23 -0
- package/lib/cjs/workouts/list/cards/WorkoutCard.js +1 -1
- package/lib/cjs/workouts/list/loaders/db.js +1 -1
- package/lib/esm/activities/base/api/fitconvert/fitconvert.js +2 -2
- package/lib/esm/activities/base/repo/db.js +2 -2
- package/lib/esm/activities/base/utils/activity.js +1 -0
- package/lib/esm/apps/base/api/velohero/api.js +1 -1
- package/lib/esm/apps/intervals/IntervalsAppConnection.js +1 -1
- package/lib/esm/apps/komoot/KomootAppConnection.js +1 -1
- package/lib/esm/apps/strava/StravaAppConnection.js +1 -1
- package/lib/esm/apps/velohero/VeloHeroAppConnection.js +1 -1
- package/lib/esm/autoupdate/api.js +15 -5
- package/lib/esm/coaches/service.js +1 -2
- package/lib/esm/devices/ride/service.js +1 -1
- package/lib/esm/maps/MapArea/MapArea.js +1 -2
- package/lib/esm/maps/MapArea/service.js +1 -0
- package/lib/esm/ride/display/service.js +1 -1
- package/lib/esm/ride/route/FreeRideDisplayService.js +1 -2
- package/lib/esm/ride/route/RLVDisplayService.js +8 -18
- package/lib/esm/routes/base/parsers/incyclist.js +1 -1
- package/lib/esm/routes/base/parsers/xml.js +1 -1
- package/lib/esm/routes/base/utils/points.js +3 -3
- package/lib/esm/routes/list/loaders/db.js +1 -1
- package/lib/esm/ui/service.js +8 -3
- package/lib/esm/video/Mp4BoxParser.js +141 -0
- package/lib/esm/video/VideoConversion.js +1 -1
- package/lib/esm/video/index.js +2 -0
- package/lib/esm/video/resolvePlaybackUrl.js +19 -0
- package/lib/esm/workouts/list/cards/WorkoutCard.js +1 -1
- package/lib/esm/workouts/list/loaders/db.js +1 -1
- package/lib/types/autoupdate/api.d.ts +2 -1
- package/lib/types/ride/route/RLVDisplayService.d.ts +1 -0
- package/lib/types/video/Mp4BoxParser.d.ts +10 -0
- package/lib/types/video/index.d.ts +2 -0
- package/lib/types/video/resolvePlaybackUrl.d.ts +1 -0
- 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 &&
|
|
269
|
-
result = result.filter(ai => ai.summary.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
|
}
|
|
@@ -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,
|
|
100
|
+
async _get(url, config = {}) {
|
|
100
101
|
const api = this.getApi();
|
|
101
102
|
const baseUrl = this.getBaseUrl();
|
|
102
|
-
return await api.get(baseUrl + url, ...
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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);
|
package/lib/cjs/ui/service.js
CHANGED
|
@@ -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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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);
|
package/lib/cjs/video/index.js
CHANGED
|
@@ -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;
|
|
@@ -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 &&
|
|
266
|
-
result = result.filter(ai => ai.summary.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
|
}
|
|
@@ -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,
|
|
97
|
+
async _get(url, config = {}) {
|
|
97
98
|
const api = this.getApi();
|
|
98
99
|
const baseUrl = this.getBaseUrl();
|
|
99
|
-
return await api.get(baseUrl + url, ...
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
80
|
+
let changed;
|
|
81
81
|
if (idx === -1) {
|
|
82
82
|
this.routeDescriptions.push(clone(updatedDescr));
|
|
83
83
|
this.routeHashes[route.description.id] = stringify(updatedDescr);
|
package/lib/esm/ui/service.js
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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);
|
package/lib/esm/video/index.js
CHANGED
|
@@ -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
|
+
};
|
|
@@ -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,
|
|
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;
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
23
|
-
"@typescript-eslint/parser": "^8.
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
24
|
+
"@typescript-eslint/parser": "^8.59.0",
|
|
24
25
|
"dotenv": "^17.4.2",
|
|
25
|
-
"eslint": "^
|
|
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.
|
|
36
|
+
"typescript-eslint": "^8.59.0"
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|
|
37
39
|
"lint": "eslint . --quiet",
|