incyclist-services 1.7.93 → 1.7.94
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/ride/route/RLVDisplayService.js +7 -2
- package/lib/cjs/routes/list/cards/RouteCard.js +2 -0
- package/lib/cjs/video/Mp4BoxParser.js +155 -3
- package/lib/cjs/video/VideoConversion.js +1 -1
- package/lib/esm/ride/route/RLVDisplayService.js +7 -2
- package/lib/esm/routes/list/cards/RouteCard.js +2 -0
- package/lib/esm/video/Mp4BoxParser.js +155 -3
- package/lib/esm/video/VideoConversion.js +1 -1
- package/lib/types/video/Mp4BoxParser.d.ts +11 -0
- package/package.json +1 -1
|
@@ -374,6 +374,10 @@ let RLVDisplayService = (() => {
|
|
|
374
374
|
if (time !== undefined) {
|
|
375
375
|
this.currentVideo.buffering = `${time}s`;
|
|
376
376
|
this.currentVideo.bufferTime = time;
|
|
377
|
+
if (time > 200) {
|
|
378
|
+
this.logEvent({ message: 'video conversion >200s' });
|
|
379
|
+
this.onVideoLoaded(0);
|
|
380
|
+
}
|
|
377
381
|
}
|
|
378
382
|
else if (progress !== undefined)
|
|
379
383
|
this.currentVideo.buffering = `${progress}%`;
|
|
@@ -390,11 +394,12 @@ let RLVDisplayService = (() => {
|
|
|
390
394
|
onVideoLoadError(error, video = this.currentVideo) {
|
|
391
395
|
video.loaded = false;
|
|
392
396
|
video.error = this.buildVideoError(error, video);
|
|
397
|
+
const isCurrentVideo = video === this.currentVideo;
|
|
393
398
|
if (video.isInitial) {
|
|
394
|
-
this.logEvent({ message: 'could not load video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error) });
|
|
399
|
+
this.logEvent({ message: 'could not load video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error), isCurrentVideo });
|
|
395
400
|
}
|
|
396
401
|
else {
|
|
397
|
-
this.logEvent({ message: 'could not load next video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error) });
|
|
402
|
+
this.logEvent({ message: 'could not load next video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error), isCurrentVideo });
|
|
398
403
|
const errIdx = this.videos.indexOf(video);
|
|
399
404
|
if (errIdx !== -1) {
|
|
400
405
|
this.videos.splice(errIdx);
|
|
@@ -165,6 +165,8 @@ let RouteCard = (() => {
|
|
|
165
165
|
path = normalizedUrl.replace('file://', '');
|
|
166
166
|
if (normalizedUrl.startsWith('video:///'))
|
|
167
167
|
path = normalizedUrl.replace('video://', '');
|
|
168
|
+
if (path && /^\/[A-Za-z]:[\\/]/.test(path))
|
|
169
|
+
path = path.slice(1);
|
|
168
170
|
if (path) {
|
|
169
171
|
return await this.fileExists(path);
|
|
170
172
|
}
|
|
@@ -65,6 +65,152 @@ const parseStsdCodec = (buf, stsdBox) => {
|
|
|
65
65
|
return undefined;
|
|
66
66
|
return buf.toString('ascii', entryOffset + 4, entryOffset + 8);
|
|
67
67
|
};
|
|
68
|
+
const CHROMA_FORMAT_NAMES = {
|
|
69
|
+
0: '4:0:0 (monochrome)', 1: '4:2:0', 2: '4:2:2', 3: '4:4:4'
|
|
70
|
+
};
|
|
71
|
+
const findSampleEntryChildBoxes = (buf, stsdBox) => {
|
|
72
|
+
const entryOffset = stsdBox.bodyStart + 8;
|
|
73
|
+
if (entryOffset + 4 > buf.length)
|
|
74
|
+
return [];
|
|
75
|
+
const entrySize = buf.readUInt32BE(entryOffset);
|
|
76
|
+
const childStart = entryOffset + 86;
|
|
77
|
+
const childEnd = Math.min(entryOffset + entrySize, buf.length);
|
|
78
|
+
if (childStart >= childEnd)
|
|
79
|
+
return [];
|
|
80
|
+
return findBoxes(buf, childStart, childEnd);
|
|
81
|
+
};
|
|
82
|
+
const AVC_PROFILE_NAMES = {
|
|
83
|
+
66: 'Baseline', 77: 'Main', 88: 'Extended', 100: 'High',
|
|
84
|
+
110: 'High 10', 122: 'High 4:2:2', 144: 'High 4:4:4', 244: 'High 4:4:4 Predictive'
|
|
85
|
+
};
|
|
86
|
+
const AVC_HIGH_FAMILY_PROFILES = new Set([100, 110, 122, 144, 244]);
|
|
87
|
+
const parseAvcC = (buf, avcCBox) => {
|
|
88
|
+
const start = avcCBox.bodyStart;
|
|
89
|
+
if (start + 4 > buf.length)
|
|
90
|
+
return undefined;
|
|
91
|
+
const profileIdc = buf.readUInt8(start + 1);
|
|
92
|
+
const levelIdc = buf.readUInt8(start + 3);
|
|
93
|
+
const details = {
|
|
94
|
+
profile: AVC_PROFILE_NAMES[profileIdc] ?? `unknown (${profileIdc})`,
|
|
95
|
+
profileId: profileIdc,
|
|
96
|
+
level: (levelIdc / 10).toFixed(1)
|
|
97
|
+
};
|
|
98
|
+
if (!AVC_HIGH_FAMILY_PROFILES.has(profileIdc))
|
|
99
|
+
return details;
|
|
100
|
+
try {
|
|
101
|
+
let offset = start + 5;
|
|
102
|
+
if (offset >= buf.length)
|
|
103
|
+
return details;
|
|
104
|
+
const numSps = buf.readUInt8(offset) & 0x1f;
|
|
105
|
+
offset += 1;
|
|
106
|
+
for (let i = 0; i < numSps; i++) {
|
|
107
|
+
if (offset + 2 > buf.length)
|
|
108
|
+
return details;
|
|
109
|
+
offset += 2 + buf.readUInt16BE(offset);
|
|
110
|
+
}
|
|
111
|
+
if (offset >= buf.length)
|
|
112
|
+
return details;
|
|
113
|
+
const numPps = buf.readUInt8(offset);
|
|
114
|
+
offset += 1;
|
|
115
|
+
for (let i = 0; i < numPps; i++) {
|
|
116
|
+
if (offset + 2 > buf.length)
|
|
117
|
+
return details;
|
|
118
|
+
offset += 2 + buf.readUInt16BE(offset);
|
|
119
|
+
}
|
|
120
|
+
if (offset + 3 > buf.length)
|
|
121
|
+
return details;
|
|
122
|
+
details.chromaFormat = CHROMA_FORMAT_NAMES[buf.readUInt8(offset) & 0x03];
|
|
123
|
+
details.bitDepthLuma = (buf.readUInt8(offset + 1) & 0x07) + 8;
|
|
124
|
+
details.bitDepthChroma = (buf.readUInt8(offset + 2) & 0x07) + 8;
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
return details;
|
|
128
|
+
};
|
|
129
|
+
const HEVC_PROFILE_NAMES = {
|
|
130
|
+
1: 'Main', 2: 'Main 10', 3: 'Main Still Picture', 4: 'Range Extensions',
|
|
131
|
+
5: 'High Throughput', 6: 'Multiview Main', 7: 'Scalable Main', 8: '3D Main',
|
|
132
|
+
9: 'Screen Content Coding', 10: 'Scalable Range Extensions'
|
|
133
|
+
};
|
|
134
|
+
const parseHvcC = (buf, hvcCBox) => {
|
|
135
|
+
const start = hvcCBox.bodyStart;
|
|
136
|
+
if (start + 19 > buf.length)
|
|
137
|
+
return undefined;
|
|
138
|
+
const byte1 = buf.readUInt8(start + 1);
|
|
139
|
+
const generalProfileIdc = byte1 & 0x1f;
|
|
140
|
+
const generalTierFlag = (byte1 >> 5) & 0x01;
|
|
141
|
+
const generalLevelIdc = buf.readUInt8(start + 12);
|
|
142
|
+
const chromaFormatIdc = buf.readUInt8(start + 16) & 0x03;
|
|
143
|
+
return {
|
|
144
|
+
profile: HEVC_PROFILE_NAMES[generalProfileIdc] ?? `unknown (${generalProfileIdc})`,
|
|
145
|
+
profileId: generalProfileIdc,
|
|
146
|
+
tier: generalTierFlag ? 'High' : 'Main',
|
|
147
|
+
level: (generalLevelIdc / 30).toFixed(1),
|
|
148
|
+
chromaFormat: CHROMA_FORMAT_NAMES[chromaFormatIdc],
|
|
149
|
+
bitDepthLuma: (buf.readUInt8(start + 17) & 0x07) + 8,
|
|
150
|
+
bitDepthChroma: (buf.readUInt8(start + 18) & 0x07) + 8
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const AV1_PROFILE_NAMES = { 0: 'Main', 1: 'High', 2: 'Professional' };
|
|
154
|
+
const av1Level = (levelIdx) => `${2 + Math.floor(levelIdx / 4)}.${levelIdx % 4}`;
|
|
155
|
+
const av1BitDepth = (highBitdepth, twelveBit) => {
|
|
156
|
+
if (twelveBit)
|
|
157
|
+
return 12;
|
|
158
|
+
return highBitdepth ? 10 : 8;
|
|
159
|
+
};
|
|
160
|
+
const av1ChromaFormat = (monochrome, subsamplingX, subsamplingY) => {
|
|
161
|
+
if (monochrome)
|
|
162
|
+
return CHROMA_FORMAT_NAMES[0];
|
|
163
|
+
if (subsamplingX && subsamplingY)
|
|
164
|
+
return '4:2:0';
|
|
165
|
+
if (subsamplingX && !subsamplingY)
|
|
166
|
+
return '4:2:2';
|
|
167
|
+
return '4:4:4';
|
|
168
|
+
};
|
|
169
|
+
const parseAv1C = (buf, av1CBox) => {
|
|
170
|
+
const start = av1CBox.bodyStart;
|
|
171
|
+
if (start + 3 > buf.length)
|
|
172
|
+
return undefined;
|
|
173
|
+
const byte1 = buf.readUInt8(start + 1);
|
|
174
|
+
const seqProfile = (byte1 >> 5) & 0x07;
|
|
175
|
+
const seqLevelIdx0 = byte1 & 0x1f;
|
|
176
|
+
const byte2 = buf.readUInt8(start + 2);
|
|
177
|
+
const seqTier0 = (byte2 >> 7) & 0x01;
|
|
178
|
+
const highBitdepth = (byte2 >> 6) & 0x01;
|
|
179
|
+
const twelveBit = (byte2 >> 5) & 0x01;
|
|
180
|
+
const monochrome = (byte2 >> 4) & 0x01;
|
|
181
|
+
const chromaSubsamplingX = (byte2 >> 3) & 0x01;
|
|
182
|
+
const chromaSubsamplingY = (byte2 >> 2) & 0x01;
|
|
183
|
+
const bitDepth = av1BitDepth(highBitdepth, twelveBit);
|
|
184
|
+
const chromaFormat = av1ChromaFormat(monochrome, chromaSubsamplingX, chromaSubsamplingY);
|
|
185
|
+
return {
|
|
186
|
+
profile: AV1_PROFILE_NAMES[seqProfile] ?? `unknown (${seqProfile})`,
|
|
187
|
+
profileId: seqProfile,
|
|
188
|
+
tier: seqTier0 ? 'High' : 'Main',
|
|
189
|
+
level: av1Level(seqLevelIdx0),
|
|
190
|
+
chromaFormat,
|
|
191
|
+
bitDepthLuma: bitDepth,
|
|
192
|
+
bitDepthChroma: bitDepth,
|
|
193
|
+
monochrome: !!monochrome
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const parseCodecDetails = (buf, codec, stsdBox) => {
|
|
197
|
+
if (!codec)
|
|
198
|
+
return undefined;
|
|
199
|
+
const childBoxes = findSampleEntryChildBoxes(buf, stsdBox);
|
|
200
|
+
if (codec === 'avc1' || codec === 'avc3') {
|
|
201
|
+
const avcCBox = findBox(childBoxes, 'avcC');
|
|
202
|
+
return avcCBox ? parseAvcC(buf, avcCBox) : undefined;
|
|
203
|
+
}
|
|
204
|
+
if (codec === 'hev1' || codec === 'hvc1') {
|
|
205
|
+
const hvcCBox = findBox(childBoxes, 'hvcC');
|
|
206
|
+
return hvcCBox ? parseHvcC(buf, hvcCBox) : undefined;
|
|
207
|
+
}
|
|
208
|
+
if (codec === 'av01') {
|
|
209
|
+
const av1CBox = findBox(childBoxes, 'av1C');
|
|
210
|
+
return av1CBox ? parseAv1C(buf, av1CBox) : undefined;
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
};
|
|
68
214
|
const parseTkhdDimensions = (buf, tkhdBox) => {
|
|
69
215
|
if (tkhdBox.bodyStart >= buf.length)
|
|
70
216
|
return {};
|
|
@@ -88,18 +234,22 @@ const parseTrak = (buf, trakBox) => {
|
|
|
88
234
|
const minfBox = findBox(mdiaChildren, 'minf');
|
|
89
235
|
const handlerType = hdlrBox ? parseHandlerType(buf, hdlrBox) : undefined;
|
|
90
236
|
let codec;
|
|
237
|
+
let codecDetails;
|
|
91
238
|
if (minfBox) {
|
|
92
239
|
const minfChildren = findBoxes(buf, minfBox.bodyStart, minfBox.bodyEnd);
|
|
93
240
|
const stblBox = findBox(minfChildren, 'stbl');
|
|
94
241
|
if (stblBox) {
|
|
95
242
|
const stblChildren = findBoxes(buf, stblBox.bodyStart, stblBox.bodyEnd);
|
|
96
243
|
const stsdBox = findBox(stblChildren, 'stsd');
|
|
97
|
-
if (stsdBox)
|
|
244
|
+
if (stsdBox) {
|
|
98
245
|
codec = parseStsdCodec(buf, stsdBox);
|
|
246
|
+
if (handlerType === 'vide')
|
|
247
|
+
codecDetails = parseCodecDetails(buf, codec, stsdBox);
|
|
248
|
+
}
|
|
99
249
|
}
|
|
100
250
|
}
|
|
101
251
|
const dimensions = (handlerType === 'vide' && tkhdBox) ? parseTkhdDimensions(buf, tkhdBox) : {};
|
|
102
|
-
return { handlerType, codec, ...dimensions };
|
|
252
|
+
return { handlerType, codec, codecDetails, ...dimensions };
|
|
103
253
|
};
|
|
104
254
|
const parseMp4Boxes = (headBuffer, tailBuffer) => {
|
|
105
255
|
const result = { moovLocation: 'not-found', incomplete: true };
|
|
@@ -130,13 +280,15 @@ const applyMoov = (buf, moovBox, result) => {
|
|
|
130
280
|
const moovChildren = findBoxes(buf, moovBox.bodyStart, moovBox.bodyEnd);
|
|
131
281
|
const trakBoxes = moovChildren.filter(b => b.type === 'trak');
|
|
132
282
|
trakBoxes.forEach(trakBox => {
|
|
133
|
-
const { handlerType, codec, width, height } = parseTrak(buf, trakBox);
|
|
283
|
+
const { handlerType, codec, width, height, codecDetails } = parseTrak(buf, trakBox);
|
|
134
284
|
if (handlerType === 'vide') {
|
|
135
285
|
result.videoCodec = codec;
|
|
136
286
|
if (width)
|
|
137
287
|
result.width = width;
|
|
138
288
|
if (height)
|
|
139
289
|
result.height = height;
|
|
290
|
+
if (codecDetails)
|
|
291
|
+
result.codecDetails = codecDetails;
|
|
140
292
|
}
|
|
141
293
|
else if (handlerType === 'soun') {
|
|
142
294
|
result.audioCodec = codec;
|
|
@@ -135,7 +135,7 @@ let VideoConversion = (() => {
|
|
|
135
135
|
const frames = mapping?.length > 0 ? Math.max(...mapping.map(m => m.frame)) : 0;
|
|
136
136
|
const framerate = this.route.details?.video?.framerate;
|
|
137
137
|
const time = framerate ? Math.round(info.frames / framerate) : parseTime(info.timemark);
|
|
138
|
-
const pct = frames ? Math.floor(info.frames / frames * 100) : undefined;
|
|
138
|
+
const pct = (frames && !isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
|
|
139
139
|
const cpu = this.getCpuStats(info);
|
|
140
140
|
if (frames || time) {
|
|
141
141
|
this.observer.emit('convert-progress', pct, info.frames, time);
|
|
@@ -371,6 +371,10 @@ let RLVDisplayService = (() => {
|
|
|
371
371
|
if (time !== undefined) {
|
|
372
372
|
this.currentVideo.buffering = `${time}s`;
|
|
373
373
|
this.currentVideo.bufferTime = time;
|
|
374
|
+
if (time > 200) {
|
|
375
|
+
this.logEvent({ message: 'video conversion >200s' });
|
|
376
|
+
this.onVideoLoaded(0);
|
|
377
|
+
}
|
|
374
378
|
}
|
|
375
379
|
else if (progress !== undefined)
|
|
376
380
|
this.currentVideo.buffering = `${progress}%`;
|
|
@@ -387,11 +391,12 @@ let RLVDisplayService = (() => {
|
|
|
387
391
|
onVideoLoadError(error, video = this.currentVideo) {
|
|
388
392
|
video.loaded = false;
|
|
389
393
|
video.error = this.buildVideoError(error, video);
|
|
394
|
+
const isCurrentVideo = video === this.currentVideo;
|
|
390
395
|
if (video.isInitial) {
|
|
391
|
-
this.logEvent({ message: 'could not load video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error) });
|
|
396
|
+
this.logEvent({ message: 'could not load video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error), isCurrentVideo });
|
|
392
397
|
}
|
|
393
398
|
else {
|
|
394
|
-
this.logEvent({ message: 'could not load next video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error) });
|
|
399
|
+
this.logEvent({ message: 'could not load next video', video: this.getVideoUrl(video), error: error.message, errorCode: this.getCode(error), isCurrentVideo });
|
|
395
400
|
const errIdx = this.videos.indexOf(video);
|
|
396
401
|
if (errIdx !== -1) {
|
|
397
402
|
this.videos.splice(errIdx);
|
|
@@ -162,6 +162,8 @@ let RouteCard = (() => {
|
|
|
162
162
|
path = normalizedUrl.replace('file://', '');
|
|
163
163
|
if (normalizedUrl.startsWith('video:///'))
|
|
164
164
|
path = normalizedUrl.replace('video://', '');
|
|
165
|
+
if (path && /^\/[A-Za-z]:[\\/]/.test(path))
|
|
166
|
+
path = path.slice(1);
|
|
165
167
|
if (path) {
|
|
166
168
|
return await this.fileExists(path);
|
|
167
169
|
}
|
|
@@ -62,6 +62,152 @@ const parseStsdCodec = (buf, stsdBox) => {
|
|
|
62
62
|
return undefined;
|
|
63
63
|
return buf.toString('ascii', entryOffset + 4, entryOffset + 8);
|
|
64
64
|
};
|
|
65
|
+
const CHROMA_FORMAT_NAMES = {
|
|
66
|
+
0: '4:0:0 (monochrome)', 1: '4:2:0', 2: '4:2:2', 3: '4:4:4'
|
|
67
|
+
};
|
|
68
|
+
const findSampleEntryChildBoxes = (buf, stsdBox) => {
|
|
69
|
+
const entryOffset = stsdBox.bodyStart + 8;
|
|
70
|
+
if (entryOffset + 4 > buf.length)
|
|
71
|
+
return [];
|
|
72
|
+
const entrySize = buf.readUInt32BE(entryOffset);
|
|
73
|
+
const childStart = entryOffset + 86;
|
|
74
|
+
const childEnd = Math.min(entryOffset + entrySize, buf.length);
|
|
75
|
+
if (childStart >= childEnd)
|
|
76
|
+
return [];
|
|
77
|
+
return findBoxes(buf, childStart, childEnd);
|
|
78
|
+
};
|
|
79
|
+
const AVC_PROFILE_NAMES = {
|
|
80
|
+
66: 'Baseline', 77: 'Main', 88: 'Extended', 100: 'High',
|
|
81
|
+
110: 'High 10', 122: 'High 4:2:2', 144: 'High 4:4:4', 244: 'High 4:4:4 Predictive'
|
|
82
|
+
};
|
|
83
|
+
const AVC_HIGH_FAMILY_PROFILES = new Set([100, 110, 122, 144, 244]);
|
|
84
|
+
const parseAvcC = (buf, avcCBox) => {
|
|
85
|
+
const start = avcCBox.bodyStart;
|
|
86
|
+
if (start + 4 > buf.length)
|
|
87
|
+
return undefined;
|
|
88
|
+
const profileIdc = buf.readUInt8(start + 1);
|
|
89
|
+
const levelIdc = buf.readUInt8(start + 3);
|
|
90
|
+
const details = {
|
|
91
|
+
profile: AVC_PROFILE_NAMES[profileIdc] ?? `unknown (${profileIdc})`,
|
|
92
|
+
profileId: profileIdc,
|
|
93
|
+
level: (levelIdc / 10).toFixed(1)
|
|
94
|
+
};
|
|
95
|
+
if (!AVC_HIGH_FAMILY_PROFILES.has(profileIdc))
|
|
96
|
+
return details;
|
|
97
|
+
try {
|
|
98
|
+
let offset = start + 5;
|
|
99
|
+
if (offset >= buf.length)
|
|
100
|
+
return details;
|
|
101
|
+
const numSps = buf.readUInt8(offset) & 0x1f;
|
|
102
|
+
offset += 1;
|
|
103
|
+
for (let i = 0; i < numSps; i++) {
|
|
104
|
+
if (offset + 2 > buf.length)
|
|
105
|
+
return details;
|
|
106
|
+
offset += 2 + buf.readUInt16BE(offset);
|
|
107
|
+
}
|
|
108
|
+
if (offset >= buf.length)
|
|
109
|
+
return details;
|
|
110
|
+
const numPps = buf.readUInt8(offset);
|
|
111
|
+
offset += 1;
|
|
112
|
+
for (let i = 0; i < numPps; i++) {
|
|
113
|
+
if (offset + 2 > buf.length)
|
|
114
|
+
return details;
|
|
115
|
+
offset += 2 + buf.readUInt16BE(offset);
|
|
116
|
+
}
|
|
117
|
+
if (offset + 3 > buf.length)
|
|
118
|
+
return details;
|
|
119
|
+
details.chromaFormat = CHROMA_FORMAT_NAMES[buf.readUInt8(offset) & 0x03];
|
|
120
|
+
details.bitDepthLuma = (buf.readUInt8(offset + 1) & 0x07) + 8;
|
|
121
|
+
details.bitDepthChroma = (buf.readUInt8(offset + 2) & 0x07) + 8;
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
return details;
|
|
125
|
+
};
|
|
126
|
+
const HEVC_PROFILE_NAMES = {
|
|
127
|
+
1: 'Main', 2: 'Main 10', 3: 'Main Still Picture', 4: 'Range Extensions',
|
|
128
|
+
5: 'High Throughput', 6: 'Multiview Main', 7: 'Scalable Main', 8: '3D Main',
|
|
129
|
+
9: 'Screen Content Coding', 10: 'Scalable Range Extensions'
|
|
130
|
+
};
|
|
131
|
+
const parseHvcC = (buf, hvcCBox) => {
|
|
132
|
+
const start = hvcCBox.bodyStart;
|
|
133
|
+
if (start + 19 > buf.length)
|
|
134
|
+
return undefined;
|
|
135
|
+
const byte1 = buf.readUInt8(start + 1);
|
|
136
|
+
const generalProfileIdc = byte1 & 0x1f;
|
|
137
|
+
const generalTierFlag = (byte1 >> 5) & 0x01;
|
|
138
|
+
const generalLevelIdc = buf.readUInt8(start + 12);
|
|
139
|
+
const chromaFormatIdc = buf.readUInt8(start + 16) & 0x03;
|
|
140
|
+
return {
|
|
141
|
+
profile: HEVC_PROFILE_NAMES[generalProfileIdc] ?? `unknown (${generalProfileIdc})`,
|
|
142
|
+
profileId: generalProfileIdc,
|
|
143
|
+
tier: generalTierFlag ? 'High' : 'Main',
|
|
144
|
+
level: (generalLevelIdc / 30).toFixed(1),
|
|
145
|
+
chromaFormat: CHROMA_FORMAT_NAMES[chromaFormatIdc],
|
|
146
|
+
bitDepthLuma: (buf.readUInt8(start + 17) & 0x07) + 8,
|
|
147
|
+
bitDepthChroma: (buf.readUInt8(start + 18) & 0x07) + 8
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const AV1_PROFILE_NAMES = { 0: 'Main', 1: 'High', 2: 'Professional' };
|
|
151
|
+
const av1Level = (levelIdx) => `${2 + Math.floor(levelIdx / 4)}.${levelIdx % 4}`;
|
|
152
|
+
const av1BitDepth = (highBitdepth, twelveBit) => {
|
|
153
|
+
if (twelveBit)
|
|
154
|
+
return 12;
|
|
155
|
+
return highBitdepth ? 10 : 8;
|
|
156
|
+
};
|
|
157
|
+
const av1ChromaFormat = (monochrome, subsamplingX, subsamplingY) => {
|
|
158
|
+
if (monochrome)
|
|
159
|
+
return CHROMA_FORMAT_NAMES[0];
|
|
160
|
+
if (subsamplingX && subsamplingY)
|
|
161
|
+
return '4:2:0';
|
|
162
|
+
if (subsamplingX && !subsamplingY)
|
|
163
|
+
return '4:2:2';
|
|
164
|
+
return '4:4:4';
|
|
165
|
+
};
|
|
166
|
+
const parseAv1C = (buf, av1CBox) => {
|
|
167
|
+
const start = av1CBox.bodyStart;
|
|
168
|
+
if (start + 3 > buf.length)
|
|
169
|
+
return undefined;
|
|
170
|
+
const byte1 = buf.readUInt8(start + 1);
|
|
171
|
+
const seqProfile = (byte1 >> 5) & 0x07;
|
|
172
|
+
const seqLevelIdx0 = byte1 & 0x1f;
|
|
173
|
+
const byte2 = buf.readUInt8(start + 2);
|
|
174
|
+
const seqTier0 = (byte2 >> 7) & 0x01;
|
|
175
|
+
const highBitdepth = (byte2 >> 6) & 0x01;
|
|
176
|
+
const twelveBit = (byte2 >> 5) & 0x01;
|
|
177
|
+
const monochrome = (byte2 >> 4) & 0x01;
|
|
178
|
+
const chromaSubsamplingX = (byte2 >> 3) & 0x01;
|
|
179
|
+
const chromaSubsamplingY = (byte2 >> 2) & 0x01;
|
|
180
|
+
const bitDepth = av1BitDepth(highBitdepth, twelveBit);
|
|
181
|
+
const chromaFormat = av1ChromaFormat(monochrome, chromaSubsamplingX, chromaSubsamplingY);
|
|
182
|
+
return {
|
|
183
|
+
profile: AV1_PROFILE_NAMES[seqProfile] ?? `unknown (${seqProfile})`,
|
|
184
|
+
profileId: seqProfile,
|
|
185
|
+
tier: seqTier0 ? 'High' : 'Main',
|
|
186
|
+
level: av1Level(seqLevelIdx0),
|
|
187
|
+
chromaFormat,
|
|
188
|
+
bitDepthLuma: bitDepth,
|
|
189
|
+
bitDepthChroma: bitDepth,
|
|
190
|
+
monochrome: !!monochrome
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
const parseCodecDetails = (buf, codec, stsdBox) => {
|
|
194
|
+
if (!codec)
|
|
195
|
+
return undefined;
|
|
196
|
+
const childBoxes = findSampleEntryChildBoxes(buf, stsdBox);
|
|
197
|
+
if (codec === 'avc1' || codec === 'avc3') {
|
|
198
|
+
const avcCBox = findBox(childBoxes, 'avcC');
|
|
199
|
+
return avcCBox ? parseAvcC(buf, avcCBox) : undefined;
|
|
200
|
+
}
|
|
201
|
+
if (codec === 'hev1' || codec === 'hvc1') {
|
|
202
|
+
const hvcCBox = findBox(childBoxes, 'hvcC');
|
|
203
|
+
return hvcCBox ? parseHvcC(buf, hvcCBox) : undefined;
|
|
204
|
+
}
|
|
205
|
+
if (codec === 'av01') {
|
|
206
|
+
const av1CBox = findBox(childBoxes, 'av1C');
|
|
207
|
+
return av1CBox ? parseAv1C(buf, av1CBox) : undefined;
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
};
|
|
65
211
|
const parseTkhdDimensions = (buf, tkhdBox) => {
|
|
66
212
|
if (tkhdBox.bodyStart >= buf.length)
|
|
67
213
|
return {};
|
|
@@ -85,18 +231,22 @@ const parseTrak = (buf, trakBox) => {
|
|
|
85
231
|
const minfBox = findBox(mdiaChildren, 'minf');
|
|
86
232
|
const handlerType = hdlrBox ? parseHandlerType(buf, hdlrBox) : undefined;
|
|
87
233
|
let codec;
|
|
234
|
+
let codecDetails;
|
|
88
235
|
if (minfBox) {
|
|
89
236
|
const minfChildren = findBoxes(buf, minfBox.bodyStart, minfBox.bodyEnd);
|
|
90
237
|
const stblBox = findBox(minfChildren, 'stbl');
|
|
91
238
|
if (stblBox) {
|
|
92
239
|
const stblChildren = findBoxes(buf, stblBox.bodyStart, stblBox.bodyEnd);
|
|
93
240
|
const stsdBox = findBox(stblChildren, 'stsd');
|
|
94
|
-
if (stsdBox)
|
|
241
|
+
if (stsdBox) {
|
|
95
242
|
codec = parseStsdCodec(buf, stsdBox);
|
|
243
|
+
if (handlerType === 'vide')
|
|
244
|
+
codecDetails = parseCodecDetails(buf, codec, stsdBox);
|
|
245
|
+
}
|
|
96
246
|
}
|
|
97
247
|
}
|
|
98
248
|
const dimensions = (handlerType === 'vide' && tkhdBox) ? parseTkhdDimensions(buf, tkhdBox) : {};
|
|
99
|
-
return { handlerType, codec, ...dimensions };
|
|
249
|
+
return { handlerType, codec, codecDetails, ...dimensions };
|
|
100
250
|
};
|
|
101
251
|
export const parseMp4Boxes = (headBuffer, tailBuffer) => {
|
|
102
252
|
const result = { moovLocation: 'not-found', incomplete: true };
|
|
@@ -126,13 +276,15 @@ const applyMoov = (buf, moovBox, result) => {
|
|
|
126
276
|
const moovChildren = findBoxes(buf, moovBox.bodyStart, moovBox.bodyEnd);
|
|
127
277
|
const trakBoxes = moovChildren.filter(b => b.type === 'trak');
|
|
128
278
|
trakBoxes.forEach(trakBox => {
|
|
129
|
-
const { handlerType, codec, width, height } = parseTrak(buf, trakBox);
|
|
279
|
+
const { handlerType, codec, width, height, codecDetails } = parseTrak(buf, trakBox);
|
|
130
280
|
if (handlerType === 'vide') {
|
|
131
281
|
result.videoCodec = codec;
|
|
132
282
|
if (width)
|
|
133
283
|
result.width = width;
|
|
134
284
|
if (height)
|
|
135
285
|
result.height = height;
|
|
286
|
+
if (codecDetails)
|
|
287
|
+
result.codecDetails = codecDetails;
|
|
136
288
|
}
|
|
137
289
|
else if (handlerType === 'soun') {
|
|
138
290
|
result.audioCodec = codec;
|
|
@@ -132,7 +132,7 @@ let VideoConversion = (() => {
|
|
|
132
132
|
const frames = mapping?.length > 0 ? Math.max(...mapping.map(m => m.frame)) : 0;
|
|
133
133
|
const framerate = this.route.details?.video?.framerate;
|
|
134
134
|
const time = framerate ? Math.round(info.frames / framerate) : parseTime(info.timemark);
|
|
135
|
-
const pct = frames ? Math.floor(info.frames / frames * 100) : undefined;
|
|
135
|
+
const pct = (frames && !isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
|
|
136
136
|
const cpu = this.getCpuStats(info);
|
|
137
137
|
if (frames || time) {
|
|
138
138
|
this.observer.emit('convert-progress', pct, info.frames, time);
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export type CodecDetails = {
|
|
2
|
+
profile?: string;
|
|
3
|
+
profileId?: number;
|
|
4
|
+
tier?: string;
|
|
5
|
+
level?: string;
|
|
6
|
+
chromaFormat?: string;
|
|
7
|
+
bitDepthLuma?: number;
|
|
8
|
+
bitDepthChroma?: number;
|
|
9
|
+
monochrome?: boolean;
|
|
10
|
+
};
|
|
1
11
|
export type Mp4CodecInfo = {
|
|
2
12
|
containerBrand?: string;
|
|
3
13
|
videoCodec?: string;
|
|
@@ -6,5 +16,6 @@ export type Mp4CodecInfo = {
|
|
|
6
16
|
height?: number;
|
|
7
17
|
moovLocation?: 'head' | 'tail' | 'not-found';
|
|
8
18
|
incomplete?: boolean;
|
|
19
|
+
codecDetails?: CodecDetails;
|
|
9
20
|
};
|
|
10
21
|
export declare const parseMp4Boxes: (headBuffer?: Buffer, tailBuffer?: Buffer) => Mp4CodecInfo;
|