@utilix-tech/sdk 0.26.0 → 0.28.0
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/CHANGELOG.md +7 -0
- package/README.md +25 -7
- package/dist/{chunk-XMKJYVKB.js → chunk-47DTCCQJ.js} +346 -1
- package/dist/{chunk-HP6C4VBX.js → chunk-I63BCOAG.js} +35 -1
- package/dist/index.cjs +379 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{media-9HEpTPGA.d.cts → media-cuxfzXxw.d.cts} +56 -2
- package/dist/{media-9HEpTPGA.d.ts → media-cuxfzXxw.d.ts} +56 -2
- package/dist/tools/media.cjs +345 -0
- package/dist/tools/media.d.cts +1 -1
- package/dist/tools/media.d.ts +1 -1
- package/dist/tools/media.js +1 -1
- package/dist/tools/units.cjs +34 -0
- package/dist/tools/units.d.cts +1 -1
- package/dist/tools/units.d.ts +1 -1
- package/dist/tools/units.js +1 -1
- package/dist/{units-D2wEimKG.d.cts → units-COt5uI20.d.cts} +27 -2
- package/dist/{units-D2wEimKG.d.ts → units-COt5uI20.d.ts} +27 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
|
|
4
4
|
|
|
5
|
+
## 0.28.0
|
|
6
|
+
- Add `readVideoInfo` (mp4-webm-video-info): duration, resolution, and video/audio codec identifiers from an MP4 (ISO BMFF moov box) or WebM (EBML/Matroska Segment) container header, with no frame decoding or transcoding.
|
|
7
|
+
|
|
8
|
+
## 0.27.0
|
|
9
|
+
- Add `calculateRuleOf72` (rule-of-72-calculator): estimate years to double an investment from a rate, or the rate needed to double in a given number of years, alongside the exact logarithmic result.
|
|
10
|
+
- Add `readFlacInfo` (flac-metadata-reader): sample rate, channels, bit depth, total samples, and duration from a FLAC file's STREAMINFO block, plus Vorbis comment tags when present.
|
|
11
|
+
|
|
5
12
|
## 0.26.0
|
|
6
13
|
- Add `computeJwkThumbprint` (jwk-thumbprint): RFC 7638 canonical thumbprint of a single JWK, hashed with SHA-256/384/512.
|
|
7
14
|
- Add `calculateVideoBitrate` (video-bitrate-calculator): resulting file size from a video/audio bitrate and duration, or the required bitrate to hit a target file size.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**579 developer utility functions for Node.js: runs locally, no API key required.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
6
6
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
@@ -272,7 +272,7 @@ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
|
|
|
272
272
|
### `/units`: Conversions
|
|
273
273
|
|
|
274
274
|
```ts
|
|
275
|
-
import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate } from "@utilix-tech/sdk/units";
|
|
275
|
+
import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72 } from "@utilix-tech/sdk/units";
|
|
276
276
|
|
|
277
277
|
// File sizes
|
|
278
278
|
convertBytes(1073741824, "GB"); // 1
|
|
@@ -358,6 +358,10 @@ calculateBreakEven({ fixedCosts: 10000, variableCostPerUnit: 20, pricePerUnit: 5
|
|
|
358
358
|
// Video file size from a bitrate + duration, or the bitrate to hit a target size
|
|
359
359
|
calculateVideoBitrate({ mode: "size-from-bitrate", durationSeconds: 600, videoBitrateKbps: 5000, audioBitrateKbps: 192 });
|
|
360
360
|
// { totalBitrateKbps: 5192, fileSizeBytes: 389400000, fileSizeMB: 389.4, fileSizeMiB: 371.36, ... }
|
|
361
|
+
|
|
362
|
+
// Rule of 72: years to double from a rate, or the rate needed to double in N years
|
|
363
|
+
calculateRuleOf72(8, "rate-to-years");
|
|
364
|
+
// { ruleOf72Estimate: 9, exactValue: 9.0065, errorPercent: -0.0718, ... }
|
|
361
365
|
```
|
|
362
366
|
|
|
363
367
|
---
|
|
@@ -557,10 +561,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
|
|
|
557
561
|
|
|
558
562
|
---
|
|
559
563
|
|
|
560
|
-
### `/media`: Image Header Parsing, EXIF, PDF, WAV, ID3 &
|
|
564
|
+
### `/media`: Image Header Parsing, EXIF, PDF, WAV, ID3, ICO, FLAC & Video Metadata
|
|
561
565
|
|
|
562
566
|
```ts
|
|
563
|
-
import { readImageInfo, readExifData, readPdfMetadata, readWavInfo, readId3Tags, readIcoInfo } from "@utilix-tech/sdk/media";
|
|
567
|
+
import { readImageInfo, readExifData, readPdfMetadata, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo } from "@utilix-tech/sdk/media";
|
|
564
568
|
|
|
565
569
|
// Reads format, dimensions, bit depth, and alpha channel straight from
|
|
566
570
|
// file bytes: no decoding, no canvas, no image library.
|
|
@@ -598,9 +602,23 @@ readId3Tags(new Uint8Array(mp3Bytes));
|
|
|
598
602
|
const icoBytes = await fs.promises.readFile("favicon.ico");
|
|
599
603
|
readIcoInfo(new Uint8Array(icoBytes));
|
|
600
604
|
// { imageCount: 3, images: [{ width: 16, height: 16, bitCount: 32, bytesInRes: 1128, ... }, ...] }
|
|
605
|
+
|
|
606
|
+
// Reads sample rate, channels, bit depth, total samples, and duration from a
|
|
607
|
+
// FLAC file's STREAMINFO block, plus Vorbis comment tags (artist, title,
|
|
608
|
+
// album, ...) from its VORBIS_COMMENT block if present.
|
|
609
|
+
const flacBytes = await fs.promises.readFile("track.flac");
|
|
610
|
+
readFlacInfo(new Uint8Array(flacBytes));
|
|
611
|
+
// { sampleRate: 44100, channels: 2, bitsPerSample: 16, durationSeconds: 214.7, artist: "Artist Name", ... }
|
|
612
|
+
|
|
613
|
+
// Reads duration, resolution, and video/audio codec identifiers from an
|
|
614
|
+
// MP4 (ISO BMFF moov box) or WebM (EBML/Matroska Segment) container
|
|
615
|
+
// header: no frame decoding, demuxing, or transcoding.
|
|
616
|
+
const videoBytes = await fs.promises.readFile("clip.mp4");
|
|
617
|
+
readVideoInfo(new Uint8Array(videoBytes));
|
|
618
|
+
// { format: "mp4", durationSeconds: 12.5, width: 1920, height: 1080, videoCodec: "avc1", audioCodec: "mp4a", ... }
|
|
601
619
|
```
|
|
602
620
|
|
|
603
|
-
`readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files.
|
|
621
|
+
`readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files. `readFlacInfo` reads the native FLAC container only, not FLAC-in-Ogg. `readVideoInfo` supports MP4 (ISO BMFF) and WebM (EBML/Matroska) containers only.
|
|
604
622
|
|
|
605
623
|
---
|
|
606
624
|
|
|
@@ -615,7 +633,7 @@ readIcoInfo(new Uint8Array(icoBytes));
|
|
|
615
633
|
| `@utilix-tech/sdk/data` | CSV, YAML, TOML, XML, INI, NDJSON parse and convert, SRT/WebVTT subtitles |
|
|
616
634
|
| `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, fake data |
|
|
617
635
|
| `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
|
|
618
|
-
| `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator |
|
|
636
|
+
| `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator, Rule of 72 calculator |
|
|
619
637
|
| `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing |
|
|
620
638
|
| `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
|
|
621
639
|
| `@utilix-tech/sdk/code` | SQL format/minify, HTML format/minify, regex tester, GraphQL format, MIME type lookup, GitHub URL parsing |
|
|
@@ -623,7 +641,7 @@ readIcoInfo(new Uint8Array(icoBytes));
|
|
|
623
641
|
| `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
|
|
624
642
|
| `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
|
|
625
643
|
| `@utilix-tech/sdk/ai_agent` | Token estimate/trim, chunk text, extract URLs/JSON/keywords, sanitize HTML, flatten/merge JSON, dedupe lines, validate schema, PII/secret/injection detect, vector similarity |
|
|
626
|
-
| `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; WAV audio info; ID3 tags; ICO favicon inspection |
|
|
644
|
+
| `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; WAV audio info; ID3 tags; ICO favicon inspection; FLAC metadata reading; MP4/WebM video info |
|
|
627
645
|
|
|
628
646
|
---
|
|
629
647
|
|
|
@@ -4,10 +4,12 @@ import { __export } from './chunk-MLKGABMK.js';
|
|
|
4
4
|
var media_exports = {};
|
|
5
5
|
__export(media_exports, {
|
|
6
6
|
readExifData: () => readExifData,
|
|
7
|
+
readFlacInfo: () => readFlacInfo,
|
|
7
8
|
readIcoInfo: () => readIcoInfo,
|
|
8
9
|
readId3Tags: () => readId3Tags,
|
|
9
10
|
readImageInfo: () => readImageInfo,
|
|
10
11
|
readPdfMetadata: () => readPdfMetadata,
|
|
12
|
+
readVideoInfo: () => readVideoInfo,
|
|
11
13
|
readWavInfo: () => readWavInfo
|
|
12
14
|
});
|
|
13
15
|
function readU32BE(bytes, offset) {
|
|
@@ -764,5 +766,348 @@ function readIcoInfo(bytes) {
|
|
|
764
766
|
}
|
|
765
767
|
return { imageCount: count, images };
|
|
766
768
|
}
|
|
769
|
+
var FLAC_TAG_FIELDS = {
|
|
770
|
+
TITLE: "title",
|
|
771
|
+
ARTIST: "artist",
|
|
772
|
+
ALBUM: "album",
|
|
773
|
+
DATE: "date",
|
|
774
|
+
GENRE: "genre",
|
|
775
|
+
TRACKNUMBER: "trackNumber"
|
|
776
|
+
};
|
|
777
|
+
function readU24BE(bytes, offset) {
|
|
778
|
+
return bytes[offset] << 16 | bytes[offset + 1] << 8 | bytes[offset + 2];
|
|
779
|
+
}
|
|
780
|
+
function readVorbisComment(bytes, start, end) {
|
|
781
|
+
let offset = start;
|
|
782
|
+
if (offset + 4 > end) return null;
|
|
783
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
784
|
+
const vendorLength = view.getUint32(offset, true);
|
|
785
|
+
offset += 4;
|
|
786
|
+
if (offset + vendorLength > end) return null;
|
|
787
|
+
const vendor = Buffer.from(bytes.subarray(offset, offset + vendorLength)).toString("utf8");
|
|
788
|
+
offset += vendorLength;
|
|
789
|
+
if (offset + 4 > end) return null;
|
|
790
|
+
const commentCount = view.getUint32(offset, true);
|
|
791
|
+
offset += 4;
|
|
792
|
+
const tags = {};
|
|
793
|
+
for (let i = 0; i < commentCount; i++) {
|
|
794
|
+
if (offset + 4 > end) break;
|
|
795
|
+
const commentLength = view.getUint32(offset, true);
|
|
796
|
+
offset += 4;
|
|
797
|
+
if (offset + commentLength > end) break;
|
|
798
|
+
const comment = Buffer.from(bytes.subarray(offset, offset + commentLength)).toString("utf8");
|
|
799
|
+
offset += commentLength;
|
|
800
|
+
const eq = comment.indexOf("=");
|
|
801
|
+
if (eq > 0) {
|
|
802
|
+
const key = comment.slice(0, eq).toUpperCase();
|
|
803
|
+
const value = comment.slice(eq + 1);
|
|
804
|
+
tags[key] = value;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return { vendor, tags };
|
|
808
|
+
}
|
|
809
|
+
function readFlacInfo(bytes) {
|
|
810
|
+
if (bytes.length < 4) return { error: "File is too small to be a valid FLAC file" };
|
|
811
|
+
if (readAscii4(bytes, 0) !== "fLaC") return { error: 'Not a FLAC file (missing "fLaC" marker)' };
|
|
812
|
+
let offset = 4;
|
|
813
|
+
let streamInfo = null;
|
|
814
|
+
let vorbisComment = null;
|
|
815
|
+
while (offset + 4 <= bytes.length) {
|
|
816
|
+
const header = bytes[offset];
|
|
817
|
+
const isLast = (header & 128) !== 0;
|
|
818
|
+
const blockType = header & 127;
|
|
819
|
+
const length = readU24BE(bytes, offset + 1);
|
|
820
|
+
const dataStart = offset + 4;
|
|
821
|
+
const dataEnd = dataStart + length;
|
|
822
|
+
if (dataEnd > bytes.length) return { error: "Truncated metadata block" };
|
|
823
|
+
if (blockType === 0) {
|
|
824
|
+
if (length < 34) return { error: "Truncated STREAMINFO block" };
|
|
825
|
+
const minBlockSize = bytes[dataStart] << 8 | bytes[dataStart + 1];
|
|
826
|
+
const maxBlockSize = bytes[dataStart + 2] << 8 | bytes[dataStart + 3];
|
|
827
|
+
const minFrameSize = readU24BE(bytes, dataStart + 4);
|
|
828
|
+
const maxFrameSize = readU24BE(bytes, dataStart + 7);
|
|
829
|
+
let packed = 0n;
|
|
830
|
+
for (let i = 0; i < 8; i++) packed = packed << 8n | BigInt(bytes[dataStart + 10 + i]);
|
|
831
|
+
const sampleRate = Number(packed >> 44n & 0xfffffn);
|
|
832
|
+
const channels = Number(packed >> 41n & 0x7n) + 1;
|
|
833
|
+
const bitsPerSample = Number(packed >> 36n & 0x1fn) + 1;
|
|
834
|
+
const totalSamples = Number(packed & 0xfffffffffn);
|
|
835
|
+
if (sampleRate <= 0) return { error: "Invalid sample rate in STREAMINFO block" };
|
|
836
|
+
streamInfo = {
|
|
837
|
+
sampleRate,
|
|
838
|
+
channels,
|
|
839
|
+
bitsPerSample,
|
|
840
|
+
totalSamples,
|
|
841
|
+
durationSeconds: Math.round(totalSamples / sampleRate * 1e3) / 1e3,
|
|
842
|
+
minBlockSize,
|
|
843
|
+
maxBlockSize,
|
|
844
|
+
minFrameSize,
|
|
845
|
+
maxFrameSize
|
|
846
|
+
};
|
|
847
|
+
} else if (blockType === 4) {
|
|
848
|
+
vorbisComment = readVorbisComment(bytes, dataStart, dataEnd);
|
|
849
|
+
}
|
|
850
|
+
offset = dataEnd;
|
|
851
|
+
if (isLast) break;
|
|
852
|
+
}
|
|
853
|
+
if (!streamInfo) return { error: "No STREAMINFO block found in this FLAC file" };
|
|
854
|
+
const result = { ...streamInfo };
|
|
855
|
+
if (vorbisComment) {
|
|
856
|
+
result.vendor = vorbisComment.vendor;
|
|
857
|
+
result.tags = vorbisComment.tags;
|
|
858
|
+
for (const [key, field] of Object.entries(FLAC_TAG_FIELDS)) {
|
|
859
|
+
if (vorbisComment.tags[key] !== void 0) result[field] = vorbisComment.tags[key];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
function readU64BE(bytes, offset) {
|
|
865
|
+
return readU32BE(bytes, offset) * 4294967296 + readU32BE(bytes, offset + 4);
|
|
866
|
+
}
|
|
867
|
+
function readMp4Boxes(bytes, start, end) {
|
|
868
|
+
const boxes = [];
|
|
869
|
+
let offset = start;
|
|
870
|
+
while (offset + 8 <= end) {
|
|
871
|
+
let size = readU32BE(bytes, offset);
|
|
872
|
+
const type = readAscii4(bytes, offset + 4);
|
|
873
|
+
let headerSize = 8;
|
|
874
|
+
if (size === 1) {
|
|
875
|
+
if (offset + 16 > end) break;
|
|
876
|
+
size = readU64BE(bytes, offset + 8);
|
|
877
|
+
headerSize = 16;
|
|
878
|
+
} else if (size === 0) {
|
|
879
|
+
size = end - offset;
|
|
880
|
+
}
|
|
881
|
+
if (size < headerSize || offset + size > end) break;
|
|
882
|
+
boxes.push({ type, start: offset + headerSize, end: offset + size });
|
|
883
|
+
offset += size;
|
|
884
|
+
}
|
|
885
|
+
return boxes;
|
|
886
|
+
}
|
|
887
|
+
function findMp4Box(boxes, type) {
|
|
888
|
+
return boxes.find((b) => b.type === type);
|
|
889
|
+
}
|
|
890
|
+
function parseMp4Mvhd(bytes, box) {
|
|
891
|
+
const o = box.start;
|
|
892
|
+
if (o >= box.end) return null;
|
|
893
|
+
const version = bytes[o];
|
|
894
|
+
if (version === 1) {
|
|
895
|
+
if (o + 32 > box.end) return null;
|
|
896
|
+
return { timescale: readU32BE(bytes, o + 20), duration: readU64BE(bytes, o + 24) };
|
|
897
|
+
}
|
|
898
|
+
if (o + 20 > box.end) return null;
|
|
899
|
+
return { timescale: readU32BE(bytes, o + 12), duration: readU32BE(bytes, o + 16) };
|
|
900
|
+
}
|
|
901
|
+
function parseMp4Tkhd(bytes, box) {
|
|
902
|
+
const o = box.start;
|
|
903
|
+
if (o >= box.end) return null;
|
|
904
|
+
const version = bytes[o];
|
|
905
|
+
const fixedFieldsSize = version === 1 ? 4 + 8 + 8 + 4 + 4 + 8 : 4 + 4 + 4 + 4 + 4 + 4;
|
|
906
|
+
const afterDuration = o + fixedFieldsSize;
|
|
907
|
+
const matrixStart = afterDuration + 8 + 2 + 2 + 2 + 2;
|
|
908
|
+
const widthOffset = matrixStart + 36;
|
|
909
|
+
if (widthOffset + 8 > box.end) return null;
|
|
910
|
+
return { width: readU32BE(bytes, widthOffset) / 65536, height: readU32BE(bytes, widthOffset + 4) / 65536 };
|
|
911
|
+
}
|
|
912
|
+
function parseMp4Hdlr(bytes, box) {
|
|
913
|
+
const o = box.start;
|
|
914
|
+
if (o + 12 > box.end) return null;
|
|
915
|
+
return readAscii4(bytes, o + 8);
|
|
916
|
+
}
|
|
917
|
+
function parseMp4StsdCodec(bytes, box) {
|
|
918
|
+
const o = box.start;
|
|
919
|
+
if (o + 8 > box.end) return null;
|
|
920
|
+
const entryCount = readU32BE(bytes, o + 4);
|
|
921
|
+
if (entryCount < 1) return null;
|
|
922
|
+
const entryStart = o + 8;
|
|
923
|
+
if (entryStart + 8 > box.end) return null;
|
|
924
|
+
return readAscii4(bytes, entryStart + 4);
|
|
925
|
+
}
|
|
926
|
+
function parseMp4StsdVideoDims(bytes, box) {
|
|
927
|
+
const o = box.start;
|
|
928
|
+
if (o + 8 > box.end) return null;
|
|
929
|
+
const entryCount = readU32BE(bytes, o + 4);
|
|
930
|
+
if (entryCount < 1) return null;
|
|
931
|
+
const entryStart = o + 8;
|
|
932
|
+
if (entryStart + 36 > box.end) return null;
|
|
933
|
+
return {
|
|
934
|
+
width: bytes[entryStart + 32] << 8 | bytes[entryStart + 33],
|
|
935
|
+
height: bytes[entryStart + 34] << 8 | bytes[entryStart + 35]
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function parseMp4(bytes) {
|
|
939
|
+
const topBoxes = readMp4Boxes(bytes, 0, bytes.length);
|
|
940
|
+
const ftyp = findMp4Box(topBoxes, "ftyp");
|
|
941
|
+
const moov = findMp4Box(topBoxes, "moov");
|
|
942
|
+
if (!moov) return { error: "No moov box found (not a valid or fully-downloaded MP4 file)" };
|
|
943
|
+
const result = { format: "mp4" };
|
|
944
|
+
if (ftyp && ftyp.start + 4 <= ftyp.end) result.majorBrand = readAscii4(bytes, ftyp.start);
|
|
945
|
+
const moovBoxes = readMp4Boxes(bytes, moov.start, moov.end);
|
|
946
|
+
const mvhd = findMp4Box(moovBoxes, "mvhd");
|
|
947
|
+
if (mvhd) {
|
|
948
|
+
const info = parseMp4Mvhd(bytes, mvhd);
|
|
949
|
+
if (info && info.timescale > 0) result.durationSeconds = Math.round(info.duration / info.timescale * 1e3) / 1e3;
|
|
950
|
+
}
|
|
951
|
+
for (const trak of moovBoxes.filter((b) => b.type === "trak")) {
|
|
952
|
+
const trakBoxes = readMp4Boxes(bytes, trak.start, trak.end);
|
|
953
|
+
const mdia = findMp4Box(trakBoxes, "mdia");
|
|
954
|
+
if (!mdia) continue;
|
|
955
|
+
const mdiaBoxes = readMp4Boxes(bytes, mdia.start, mdia.end);
|
|
956
|
+
const hdlr = findMp4Box(mdiaBoxes, "hdlr");
|
|
957
|
+
const handlerType = hdlr ? parseMp4Hdlr(bytes, hdlr) : null;
|
|
958
|
+
const minf = findMp4Box(mdiaBoxes, "minf");
|
|
959
|
+
if (!minf) continue;
|
|
960
|
+
const stbl = findMp4Box(readMp4Boxes(bytes, minf.start, minf.end), "stbl");
|
|
961
|
+
if (!stbl) continue;
|
|
962
|
+
const stsd = findMp4Box(readMp4Boxes(bytes, stbl.start, stbl.end), "stsd");
|
|
963
|
+
const codec = stsd ? parseMp4StsdCodec(bytes, stsd) : null;
|
|
964
|
+
if (handlerType === "vide") {
|
|
965
|
+
if (codec) result.videoCodec = codec;
|
|
966
|
+
const tkhd = findMp4Box(trakBoxes, "tkhd");
|
|
967
|
+
const dims = tkhd ? parseMp4Tkhd(bytes, tkhd) : null;
|
|
968
|
+
if (dims && dims.width > 0 && dims.height > 0) {
|
|
969
|
+
result.width = Math.round(dims.width);
|
|
970
|
+
result.height = Math.round(dims.height);
|
|
971
|
+
} else if (stsd) {
|
|
972
|
+
const pixelDims = parseMp4StsdVideoDims(bytes, stsd);
|
|
973
|
+
if (pixelDims && pixelDims.width > 0 && pixelDims.height > 0) {
|
|
974
|
+
result.width = pixelDims.width;
|
|
975
|
+
result.height = pixelDims.height;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
} else if (handlerType === "soun") {
|
|
979
|
+
if (codec) result.audioCodec = codec;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (result.durationSeconds === void 0 && !result.videoCodec && !result.audioCodec) {
|
|
983
|
+
return { error: "Could not find any usable track metadata in this MP4 file's moov box" };
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
function ebmlVintLength(firstByte) {
|
|
988
|
+
if (firstByte === 0) return 0;
|
|
989
|
+
let mask = 128;
|
|
990
|
+
let length = 1;
|
|
991
|
+
while (length <= 8 && (firstByte & mask) === 0) {
|
|
992
|
+
mask >>= 1;
|
|
993
|
+
length++;
|
|
994
|
+
}
|
|
995
|
+
return length;
|
|
996
|
+
}
|
|
997
|
+
function readEbmlId(bytes, offset) {
|
|
998
|
+
if (offset >= bytes.length) return null;
|
|
999
|
+
const length = ebmlVintLength(bytes[offset]);
|
|
1000
|
+
if (length === 0 || offset + length > bytes.length) return null;
|
|
1001
|
+
let id = 0;
|
|
1002
|
+
for (let i = 0; i < length; i++) id = id * 256 + bytes[offset + i];
|
|
1003
|
+
return { id, length };
|
|
1004
|
+
}
|
|
1005
|
+
function readEbmlSize(bytes, offset) {
|
|
1006
|
+
if (offset >= bytes.length) return null;
|
|
1007
|
+
const length = ebmlVintLength(bytes[offset]);
|
|
1008
|
+
if (length === 0 || offset + length > bytes.length) return null;
|
|
1009
|
+
const mask = 255 >> length;
|
|
1010
|
+
let value = bytes[offset] & mask;
|
|
1011
|
+
let allOnes = value === mask;
|
|
1012
|
+
for (let i = 1; i < length; i++) {
|
|
1013
|
+
value = value * 256 + bytes[offset + i];
|
|
1014
|
+
if (bytes[offset + i] !== 255) allOnes = false;
|
|
1015
|
+
}
|
|
1016
|
+
return { value, length, unknown: allOnes };
|
|
1017
|
+
}
|
|
1018
|
+
function readEbmlUint(bytes, offset, length) {
|
|
1019
|
+
let value = 0;
|
|
1020
|
+
for (let i = 0; i < length; i++) value = value * 256 + bytes[offset + i];
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
function readEbmlFloat(bytes, offset, length) {
|
|
1024
|
+
if (offset + length > bytes.length) return 0;
|
|
1025
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, length);
|
|
1026
|
+
if (length === 4) return view.getFloat32(0, false);
|
|
1027
|
+
if (length === 8) return view.getFloat64(0, false);
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
function ebmlChildren(bytes, start, end) {
|
|
1031
|
+
const children = [];
|
|
1032
|
+
let offset = start;
|
|
1033
|
+
while (offset < end) {
|
|
1034
|
+
const idInfo = readEbmlId(bytes, offset);
|
|
1035
|
+
if (!idInfo) break;
|
|
1036
|
+
const sizeInfo = readEbmlSize(bytes, offset + idInfo.length);
|
|
1037
|
+
if (!sizeInfo) break;
|
|
1038
|
+
const dataStart = offset + idInfo.length + sizeInfo.length;
|
|
1039
|
+
const dataEnd = sizeInfo.unknown ? end : Math.min(dataStart + sizeInfo.value, end);
|
|
1040
|
+
children.push({ id: idInfo.id, dataStart, dataEnd });
|
|
1041
|
+
offset = dataEnd;
|
|
1042
|
+
}
|
|
1043
|
+
return children;
|
|
1044
|
+
}
|
|
1045
|
+
var EBML_ID_SEGMENT = 408125543;
|
|
1046
|
+
var EBML_ID_INFO = 357149030;
|
|
1047
|
+
var EBML_ID_TIMECODE_SCALE = 2807729;
|
|
1048
|
+
var EBML_ID_DURATION = 17545;
|
|
1049
|
+
var EBML_ID_TRACKS = 374648427;
|
|
1050
|
+
var EBML_ID_TRACK_ENTRY = 174;
|
|
1051
|
+
var EBML_ID_TRACK_TYPE = 131;
|
|
1052
|
+
var EBML_ID_CODEC_ID = 134;
|
|
1053
|
+
var EBML_ID_VIDEO = 224;
|
|
1054
|
+
var EBML_ID_PIXEL_WIDTH = 176;
|
|
1055
|
+
var EBML_ID_PIXEL_HEIGHT = 186;
|
|
1056
|
+
var EBML_ID_CLUSTER = 524531317;
|
|
1057
|
+
function readVideoInfo(bytes) {
|
|
1058
|
+
if (bytes.length < 12) return { error: "File is too small to be a valid video file" };
|
|
1059
|
+
if (readAscii4(bytes, 4) === "ftyp") return parseMp4(bytes);
|
|
1060
|
+
if (bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) return parseWebm(bytes);
|
|
1061
|
+
return { error: 'Not a recognized MP4 or WebM file (missing an "ftyp" box or an EBML header signature)' };
|
|
1062
|
+
}
|
|
1063
|
+
function parseWebm(bytes) {
|
|
1064
|
+
const top = ebmlChildren(bytes, 0, bytes.length);
|
|
1065
|
+
const segment = top.find((c) => c.id === EBML_ID_SEGMENT);
|
|
1066
|
+
if (!segment) return { error: "No Segment element found (not a valid WebM/Matroska file)" };
|
|
1067
|
+
const result = { format: "webm" };
|
|
1068
|
+
let timecodeScale = 1e6;
|
|
1069
|
+
let rawDuration;
|
|
1070
|
+
for (const child of ebmlChildren(bytes, segment.dataStart, segment.dataEnd)) {
|
|
1071
|
+
if (child.id === EBML_ID_CLUSTER) break;
|
|
1072
|
+
if (child.id === EBML_ID_INFO) {
|
|
1073
|
+
for (const f of ebmlChildren(bytes, child.dataStart, child.dataEnd)) {
|
|
1074
|
+
if (f.id === EBML_ID_TIMECODE_SCALE) timecodeScale = readEbmlUint(bytes, f.dataStart, f.dataEnd - f.dataStart) || 1e6;
|
|
1075
|
+
else if (f.id === EBML_ID_DURATION) rawDuration = readEbmlFloat(bytes, f.dataStart, f.dataEnd - f.dataStart);
|
|
1076
|
+
}
|
|
1077
|
+
} else if (child.id === EBML_ID_TRACKS) {
|
|
1078
|
+
for (const entry of ebmlChildren(bytes, child.dataStart, child.dataEnd)) {
|
|
1079
|
+
if (entry.id !== EBML_ID_TRACK_ENTRY) continue;
|
|
1080
|
+
let trackType;
|
|
1081
|
+
let codecId;
|
|
1082
|
+
let pixelWidth;
|
|
1083
|
+
let pixelHeight;
|
|
1084
|
+
for (const f of ebmlChildren(bytes, entry.dataStart, entry.dataEnd)) {
|
|
1085
|
+
if (f.id === EBML_ID_TRACK_TYPE) trackType = readEbmlUint(bytes, f.dataStart, f.dataEnd - f.dataStart);
|
|
1086
|
+
else if (f.id === EBML_ID_CODEC_ID) codecId = Buffer.from(bytes.subarray(f.dataStart, f.dataEnd)).toString("ascii").replace(/\0+$/, "");
|
|
1087
|
+
else if (f.id === EBML_ID_VIDEO) {
|
|
1088
|
+
for (const v of ebmlChildren(bytes, f.dataStart, f.dataEnd)) {
|
|
1089
|
+
if (v.id === EBML_ID_PIXEL_WIDTH) pixelWidth = readEbmlUint(bytes, v.dataStart, v.dataEnd - v.dataStart);
|
|
1090
|
+
else if (v.id === EBML_ID_PIXEL_HEIGHT) pixelHeight = readEbmlUint(bytes, v.dataStart, v.dataEnd - v.dataStart);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (trackType === 1) {
|
|
1095
|
+
if (codecId) result.videoCodec = codecId;
|
|
1096
|
+
if (pixelWidth) result.width = pixelWidth;
|
|
1097
|
+
if (pixelHeight) result.height = pixelHeight;
|
|
1098
|
+
} else if (trackType === 2) {
|
|
1099
|
+
if (codecId) result.audioCodec = codecId;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (rawDuration !== void 0 && timecodeScale > 0) {
|
|
1105
|
+
result.durationSeconds = Math.round(rawDuration * timecodeScale / 1e9 * 1e3) / 1e3;
|
|
1106
|
+
}
|
|
1107
|
+
if (result.durationSeconds === void 0 && !result.videoCodec && !result.audioCodec) {
|
|
1108
|
+
return { error: "Could not find any usable metadata in this WebM file's Segment element" };
|
|
1109
|
+
}
|
|
1110
|
+
return result;
|
|
1111
|
+
}
|
|
767
1112
|
|
|
768
|
-
export { media_exports, readExifData, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readWavInfo };
|
|
1113
|
+
export { media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readVideoInfo, readWavInfo };
|
|
@@ -25,6 +25,7 @@ __export(units_exports, {
|
|
|
25
25
|
calculateLoan: () => calculateLoan,
|
|
26
26
|
calculateMarginMarkup: () => calculateMarginMarkup,
|
|
27
27
|
calculatePercentageChange: () => calculatePercentageChange,
|
|
28
|
+
calculateRuleOf72: () => calculateRuleOf72,
|
|
28
29
|
calculateSimpleInterest: () => calculateSimpleInterest,
|
|
29
30
|
calculateTax: () => calculateTax,
|
|
30
31
|
calculateTimeValue: () => calculateTimeValue,
|
|
@@ -1642,5 +1643,38 @@ function calculateVideoBitrate(input) {
|
|
|
1642
1643
|
}
|
|
1643
1644
|
return { error: 'mode must be "size-from-bitrate" or "bitrate-from-size"' };
|
|
1644
1645
|
}
|
|
1646
|
+
function calculateRuleOf72(value, direction) {
|
|
1647
|
+
if (!Number.isFinite(value) || value <= 0) return { error: "Value must be a positive number" };
|
|
1648
|
+
if (direction !== "rate-to-years" && direction !== "years-to-rate") {
|
|
1649
|
+
return { error: 'Direction must be "rate-to-years" or "years-to-rate"' };
|
|
1650
|
+
}
|
|
1651
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
1652
|
+
if (direction === "rate-to-years") {
|
|
1653
|
+
const ratePercent = value;
|
|
1654
|
+
const ruleOf72Estimate2 = 72 / ratePercent;
|
|
1655
|
+
const exactValue2 = Math.log(2) / Math.log(1 + ratePercent / 100);
|
|
1656
|
+
const errorPercent2 = (ruleOf72Estimate2 - exactValue2) / exactValue2 * 100;
|
|
1657
|
+
return {
|
|
1658
|
+
direction,
|
|
1659
|
+
ratePercent: round4(ratePercent),
|
|
1660
|
+
years: round4(exactValue2),
|
|
1661
|
+
ruleOf72Estimate: round4(ruleOf72Estimate2),
|
|
1662
|
+
exactValue: round4(exactValue2),
|
|
1663
|
+
errorPercent: round4(errorPercent2)
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
const years = value;
|
|
1667
|
+
const ruleOf72Estimate = 72 / years;
|
|
1668
|
+
const exactValue = (Math.pow(2, 1 / years) - 1) * 100;
|
|
1669
|
+
const errorPercent = (ruleOf72Estimate - exactValue) / exactValue * 100;
|
|
1670
|
+
return {
|
|
1671
|
+
direction,
|
|
1672
|
+
ratePercent: round4(exactValue),
|
|
1673
|
+
years: round4(years),
|
|
1674
|
+
ruleOf72Estimate: round4(ruleOf72Estimate),
|
|
1675
|
+
exactValue: round4(exactValue),
|
|
1676
|
+
errorPercent: round4(errorPercent)
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1645
1679
|
|
|
1646
|
-
export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCompoundInterest, calculateDiscount, calculateLoan, calculateMarginMarkup, calculatePercentageChange, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
|
|
1680
|
+
export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCompoundInterest, calculateDiscount, calculateLoan, calculateMarginMarkup, calculatePercentageChange, calculateRuleOf72, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
|