diff --git a/dev/convert.html b/dev/convert.html
index a58ddec..cf6f001 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -56,7 +56,7 @@
mimeType: 'image/jpeg'
}],
trackNumber: 4,
- trackNumberMax: 10,
+ tracksTotal: 10,
discNumber: 5,
discNumberMax: 8,
lyrics: "There's no way\nThat it's not going there",
diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts
index 63a5ceb..5985df0 100644
--- a/src/adts/adts-demuxer.ts
+++ b/src/adts/adts-demuxer.ts
@@ -126,8 +126,8 @@ export class AdtsDemuxer extends Demuxer {
return track.computeDuration();
}
- async getMetadata() {
- return {}; // No metadata in this one
+ async getMetadataTags() {
+ return {}; // No tags in this one
}
}
diff --git a/src/demuxer.ts b/src/demuxer.ts
index 75e2803..f654dab 100644
--- a/src/demuxer.ts
+++ b/src/demuxer.ts
@@ -8,7 +8,7 @@
import { Input } from './input';
import { InputTrack } from './input-track';
-import { MediaMetadata } from './metadata';
+import { MetadataTags } from './tags';
export abstract class Demuxer {
input: Input;
@@ -20,5 +20,5 @@ export abstract class Demuxer {
abstract computeDuration(): Promise;
abstract getTracks(): Promise;
abstract getMimeType(): Promise;
- abstract getMetadata(): Promise;
+ abstract getMetadataTags(): Promise;
}
diff --git a/src/input.ts b/src/input.ts
index bc52896..f7fe149 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -144,6 +144,6 @@ export class Input {
async getMetadata() {
const demuxer = await this._getDemuxer();
- return demuxer.getMetadata();
+ return demuxer.getMetadataTags();
}
}
diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts
index 823ceda..c15b05f 100644
--- a/src/isobmff/isobmff-boxes.ts
+++ b/src/isobmff/isobmff-boxes.ts
@@ -44,7 +44,7 @@ import {
Sample,
} from './isobmff-muxer';
import { parseOpusIdentificationHeader } from '../codec-data';
-import { MediaMetadata, RichImageData } from '../metadata';
+import { MetadataTags, RichImageData } from '../tags';
export class IsobmffBoxWriter {
private helper = new Uint8Array(8);
@@ -1274,11 +1274,11 @@ export const vtta = (notes: string) => box('vtta', [...textEncoder.encode(notes)
const udta = (muxer: IsobmffMuxer) => {
const boxes: Box[] = [];
- // Depending on the format, metadata is written differently
+ // Depending on the format, metadata tags are written differently
if (muxer.isQuickTime) {
- addQuickTimeMetadataboxes(boxes, muxer.output._metadata);
+ addQuickTimeMetadataTagBoxes(boxes, muxer.output._metadataTags);
} else {
- const metaBox = meta(muxer.output._metadata);
+ const metaBox = meta(muxer.output._metadataTags);
if (metaBox) {
boxes.push(metaBox);
}
@@ -1291,46 +1291,46 @@ const udta = (muxer: IsobmffMuxer) => {
return box('udta', undefined, boxes);
};
-const addQuickTimeMetadataboxes = (boxes: Box[], metadata: MediaMetadata) => {
+const addQuickTimeMetadataTagBoxes = (boxes: Box[], tags: MetadataTags) => {
// https://exiftool.org/TagNames/QuickTime.html (QuickTime UserData Tags)
- // For QuickTime files, metadata is dumped into the udta box
+ // For QuickTime files, metadata tags are dumped into the udta box
- for (const { key, value } of keyValueIterator(metadata)) {
+ for (const { key, value } of keyValueIterator(tags)) {
switch (key) {
case 'title': {
- boxes.push(metadataStringBoxShort('©nam', value));
+ boxes.push(metadataTagStringBoxShort('©nam', value));
}; break;
case 'description': {
- boxes.push(metadataStringBoxShort('©des', value));
+ boxes.push(metadataTagStringBoxShort('©des', value));
}; break;
case 'artist': {
- boxes.push(metadataStringBoxShort('©ART', value));
+ boxes.push(metadataTagStringBoxShort('©ART', value));
}; break;
case 'album': {
- boxes.push(metadataStringBoxShort('©alb', value));
+ boxes.push(metadataTagStringBoxShort('©alb', value));
}; break;
case 'albumArtist': {
- boxes.push(metadataStringBoxShort('albr', value));
+ boxes.push(metadataTagStringBoxShort('albr', value));
}; break;
case 'genre': {
- boxes.push(metadataStringBoxShort('©gen', value));
+ boxes.push(metadataTagStringBoxShort('©gen', value));
}; break;
case 'date': {
- boxes.push(metadataStringBoxShort('©day', value.toISOString().slice(0, 10)));
+ boxes.push(metadataTagStringBoxShort('©day', value.toISOString().slice(0, 10)));
}; break;
case 'comment': {
- boxes.push(metadataStringBoxShort('©cmt', value));
+ boxes.push(metadataTagStringBoxShort('©cmt', value));
}; break;
case 'lyrics': {
- boxes.push(metadataStringBoxShort('©lyr', value));
+ boxes.push(metadataTagStringBoxShort('©lyr', value));
}; break;
case 'raw': {
@@ -1338,9 +1338,9 @@ const addQuickTimeMetadataboxes = (boxes: Box[], metadata: MediaMetadata) => {
}; break;
case 'discNumber':
- case 'discNumberMax':
+ case 'discsTotal':
case 'trackNumber':
- case 'trackNumberMax':
+ case 'tracksTotal':
case 'images': {
// Not written for QuickTime (common Apple L)
}; break;
@@ -1349,15 +1349,15 @@ const addQuickTimeMetadataboxes = (boxes: Box[], metadata: MediaMetadata) => {
}
}
- if (metadata.raw) {
- for (const key in metadata.raw) {
- const value = metadata.raw[key];
+ if (tags.raw) {
+ for (const key in tags.raw) {
+ const value = tags.raw[key];
if (value == null || key.length !== 4 || boxes.some(x => x.type === key)) {
continue;
}
if (typeof value === 'string') {
- boxes.push(metadataStringBoxShort(key, value));
+ boxes.push(metadataTagStringBoxShort(key, value));
} else if (value instanceof Uint8Array) {
boxes.push(box(key, Array.from(value)));
}
@@ -1365,7 +1365,7 @@ const addQuickTimeMetadataboxes = (boxes: Box[], metadata: MediaMetadata) => {
}
};
-const metadataStringBoxShort = (name: string, value: string) => {
+const metadataTagStringBoxShort = (name: string, value: string) => {
const encoded = textEncoder.encode(value);
return box(name, [
@@ -1382,48 +1382,48 @@ const DATA_BOX_MIME_TYPE_MAP: Record = {
};
/** Metadata Box */
-const meta = (metadata: MediaMetadata) => {
+const meta = (tags: MetadataTags) => {
const boxes: Box[] = [];
// https://exiftool.org/TagNames/QuickTime.html (QuickTime ItemList Tags)
// This is the metadata format used for MP4 files
- for (const { key, value } of keyValueIterator(metadata)) {
+ for (const { key, value } of keyValueIterator(tags)) {
switch (key) {
case 'title': {
- boxes.push(metadataStringBoxLong('©nam', value));
+ boxes.push(metadataTagStringBoxLong('©nam', value));
}; break;
case 'description': {
- boxes.push(metadataStringBoxLong('©des', value));
+ boxes.push(metadataTagStringBoxLong('©des', value));
}; break;
case 'artist': {
- boxes.push(metadataStringBoxLong('©ART', value));
+ boxes.push(metadataTagStringBoxLong('©ART', value));
}; break;
case 'album': {
- boxes.push(metadataStringBoxLong('©alb', value));
+ boxes.push(metadataTagStringBoxLong('©alb', value));
}; break;
case 'albumArtist': {
- boxes.push(metadataStringBoxLong('aART', value));
+ boxes.push(metadataTagStringBoxLong('aART', value));
}; break;
case 'comment': {
- boxes.push(metadataStringBoxLong('©cmt', value));
+ boxes.push(metadataTagStringBoxLong('©cmt', value));
}; break;
case 'genre': {
- boxes.push(metadataStringBoxLong('©gen', value));
+ boxes.push(metadataTagStringBoxLong('©gen', value));
}; break;
case 'lyrics': {
- boxes.push(metadataStringBoxLong('©lyr', value));
+ boxes.push(metadataTagStringBoxLong('©lyr', value));
}; break;
case 'date': {
- boxes.push(metadataStringBoxLong('©day', value.toISOString().slice(0, 10)));
+ boxes.push(metadataTagStringBoxLong('©day', value.toISOString().slice(0, 10)));
}; break;
case 'images': {
@@ -1449,7 +1449,7 @@ const meta = (metadata: MediaMetadata) => {
u32(0),
u16(0), // Empty
u16(value),
- u16(metadata.trackNumberMax ?? 0),
+ u16(tags.tracksTotal ?? 0),
u16(0), // Empty
]),
]));
@@ -1462,14 +1462,14 @@ const meta = (metadata: MediaMetadata) => {
u32(0),
u16(0), // Empty
u16(value),
- u16(metadata.discNumberMax ?? 0),
+ u16(tags.discsTotal ?? 0),
u16(0), // Empty
]),
]));
}; break;
- case 'trackNumberMax':
- case 'discNumberMax':{
+ case 'tracksTotal':
+ case 'discsTotal':{
// These are included with 'trackNumber' and 'discNumber' respectively
}; break;
@@ -1481,15 +1481,15 @@ const meta = (metadata: MediaMetadata) => {
}
}
- if (metadata.raw) {
- for (const key in metadata.raw) {
- const value = metadata.raw[key];
+ if (tags.raw) {
+ for (const key in tags.raw) {
+ const value = tags.raw[key];
if (value == null || key.length !== 4 || boxes.some(x => x.type === key)) {
continue;
}
if (typeof value === 'string') {
- boxes.push(metadataStringBoxLong(key, value));
+ boxes.push(metadataTagStringBoxLong(key, value));
} else if (value instanceof Uint8Array) {
boxes.push(box(key, undefined, [
box('data', [
@@ -1520,7 +1520,7 @@ const meta = (metadata: MediaMetadata) => {
]);
};
-const metadataStringBoxLong = (name: string, value: string) => {
+const metadataTagStringBoxLong = (name: string, value: string) => {
return box(name, undefined, [
box('data', [
u32(1), // Type indicator (UTF-8)
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index 431bd30..fddbb3e 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -86,7 +86,7 @@ import {
readU8,
readAscii,
} from '../reader';
-import { MediaMetadata, RichImageData } from '../metadata';
+import { MetadataTags, RichImageData } from '../tags';
// https://exiftool.org/TagNames/QuickTime.html
const UDTA_STRING_KEYS = new Set([
@@ -249,7 +249,7 @@ export class IsobmffDemuxer extends Demuxer {
movieTimescale = -1;
movieDurationInTimescale = -1;
isQuickTime = false;
- metadata: MediaMetadata = {};
+ metadataTags: MetadataTags = {};
currentMetadataKeys: Map | null = null;
isFragmented = false;
@@ -288,9 +288,9 @@ export class IsobmffDemuxer extends Demuxer {
});
}
- async getMetadata() {
+ async getMetadataTags() {
await this.readMetadata();
- return this.metadata;
+ return this.metadataTags;
}
readMetadata() {
@@ -2057,12 +2057,12 @@ export class IsobmffDemuxer extends Demuxer {
for (const { boxInfo, slice } of iterator) {
if (boxInfo.name !== 'meta' && !this.currentTrack) {
const startPos = slice.filePos;
- this.metadata.raw ??= {};
+ this.metadataTags.raw ??= {};
if (UDTA_STRING_KEYS.has(boxInfo.name)) {
- this.metadata.raw[boxInfo.name] ??= readMetadataStringShort(slice);
+ this.metadataTags.raw[boxInfo.name] ??= readMetadataStringShort(slice);
} else {
- this.metadata.raw[boxInfo.name] ??= readBytes(slice, boxInfo.contentSize);
+ this.metadataTags.raw[boxInfo.name] ??= readBytes(slice, boxInfo.contentSize);
}
slice.filePos = startPos;
@@ -2079,37 +2079,37 @@ export class IsobmffDemuxer extends Demuxer {
if (this.currentTrack) {
this.currentTrack.name = textDecoder.decode(readBytes(slice, boxInfo.contentSize));
} else {
- this.metadata.title ??= readMetadataStringShort(slice);
+ this.metadataTags.title ??= readMetadataStringShort(slice);
}
}; break;
case '©des': {
if (!this.currentTrack) {
- this.metadata.description ??= readMetadataStringShort(slice);
+ this.metadataTags.description ??= readMetadataStringShort(slice);
}
}; break;
case '©ART': {
if (!this.currentTrack) {
- this.metadata.artist ??= readMetadataStringShort(slice);
+ this.metadataTags.artist ??= readMetadataStringShort(slice);
}
}; break;
case '©alb': {
if (!this.currentTrack) {
- this.metadata.album ??= readMetadataStringShort(slice);
+ this.metadataTags.album ??= readMetadataStringShort(slice);
}
}; break;
case 'albr': {
if (!this.currentTrack) {
- this.metadata.albumArtist ??= readMetadataStringShort(slice);
+ this.metadataTags.albumArtist ??= readMetadataStringShort(slice);
}
}; break;
case '©gen': {
if (!this.currentTrack) {
- this.metadata.genre ??= readMetadataStringShort(slice);
+ this.metadataTags.genre ??= readMetadataStringShort(slice);
}
}; break;
@@ -2117,20 +2117,20 @@ export class IsobmffDemuxer extends Demuxer {
if (!this.currentTrack) {
const date = new Date(readMetadataStringShort(slice));
if (!Number.isNaN(date.getTime())) {
- this.metadata.date ??= date;
+ this.metadataTags.date ??= date;
}
}
}; break;
case '©cmt': {
if (!this.currentTrack) {
- this.metadata.comment ??= readMetadataStringShort(slice);
+ this.metadataTags.comment ??= readMetadataStringShort(slice);
}
}; break;
case '©lyr': {
if (!this.currentTrack) {
- this.metadata.lyrics ??= readMetadataStringShort(slice);
+ this.metadataTags.lyrics ??= readMetadataStringShort(slice);
}
}; break;
}
@@ -2199,8 +2199,8 @@ export class IsobmffDemuxer extends Demuxer {
const data = readDataBox(slice);
- this.metadata.raw ??= {};
- this.metadata.raw[metadataKey] ??= data;
+ this.metadataTags.raw ??= {};
+ this.metadataTags.raw[metadataKey] ??= data;
switch (metadataKey) {
case '©nam':
@@ -2208,7 +2208,7 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.title':
case 'title': {
if (typeof data === 'string') {
- this.metadata.title ??= data;
+ this.metadataTags.title ??= data;
}
}; break;
@@ -2218,7 +2218,7 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.description':
case 'description': {
if (typeof data === 'string') {
- this.metadata.description ??= data;
+ this.metadataTags.description ??= data;
}
}; break;
@@ -2226,7 +2226,7 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.artist':
case 'artist': {
if (typeof data === 'string') {
- this.metadata.artist ??= data;
+ this.metadataTags.artist ??= data;
}
}; break;
@@ -2235,14 +2235,14 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.album':
case 'album': {
if (typeof data === 'string') {
- this.metadata.album ??= data;
+ this.metadataTags.album ??= data;
}
}; break;
case 'aART':
case 'album_artist': {
if (typeof data === 'string') {
- this.metadata.albumArtist ??= data;
+ this.metadataTags.albumArtist ??= data;
}
}; break;
@@ -2250,7 +2250,7 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.comment':
case 'comment': {
if (typeof data === 'string') {
- this.metadata.comment ??= data;
+ this.metadataTags.comment ??= data;
}
}; break;
@@ -2259,14 +2259,14 @@ export class IsobmffDemuxer extends Demuxer {
case 'com.apple.quicktime.genre':
case 'genre': {
if (typeof data === 'string') {
- this.metadata.genre ??= data;
+ this.metadataTags.genre ??= data;
}
}; break;
case '©lyr':
case 'lyrics': {
if (typeof data === 'string') {
- this.metadata.lyrics ??= data;
+ this.metadataTags.lyrics ??= data;
}
}; break;
@@ -2277,7 +2277,7 @@ export class IsobmffDemuxer extends Demuxer {
if (typeof data === 'string') {
const date = new Date(data);
if (!Number.isNaN(date.getTime())) {
- this.metadata.date ??= date;
+ this.metadataTags.date ??= date;
}
}
}; break;
@@ -2285,15 +2285,15 @@ export class IsobmffDemuxer extends Demuxer {
case 'covr':
case 'com.apple.quicktime.artwork': {
if (data instanceof RichImageData) {
- this.metadata.images ??= [];
- this.metadata.images.push({
+ this.metadataTags.images ??= [];
+ this.metadataTags.images.push({
data: data.data,
kind: 'coverFront',
mimeType: data.mimeType,
});
} else if (data instanceof Uint8Array) {
- this.metadata.images ??= [];
- this.metadata.images.push({
+ this.metadataTags.images ??= [];
+ this.metadataTags.images.push({
data,
kind: 'coverFront',
mimeType: 'image/*',
@@ -2306,13 +2306,13 @@ export class IsobmffDemuxer extends Demuxer {
const view = toDataView(data);
const trackNumber = view.getUint16(2, false);
- const trackNumberMax = view.getUint16(4, false);
+ const tracksTotal = view.getUint16(4, false);
if (trackNumber > 0) {
- this.metadata.trackNumber ??= trackNumber;
+ this.metadataTags.trackNumber ??= trackNumber;
}
- if (trackNumberMax > 0) {
- this.metadata.trackNumberMax ??= trackNumberMax;
+ if (tracksTotal > 0) {
+ this.metadataTags.tracksTotal ??= tracksTotal;
}
}
}; break;
@@ -2326,10 +2326,10 @@ export class IsobmffDemuxer extends Demuxer {
const discNumberMax = view.getUint16(4, false);
if (discNumber > 0) {
- this.metadata.discNumber ??= discNumber;
+ this.metadataTags.discNumber ??= discNumber;
}
if (discNumberMax > 0) {
- this.metadata.discNumberMax ??= discNumberMax;
+ this.metadataTags.discsTotal ??= discNumberMax;
}
}
}; break;
diff --git a/src/isobmff/isobmff-reader.ts b/src/isobmff/isobmff-reader.ts
index 3aa5170..584a0a8 100644
--- a/src/isobmff/isobmff-reader.ts
+++ b/src/isobmff/isobmff-reader.ts
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-import { RichImageData } from '../metadata';
+import { RichImageData } from '../tags';
import { textDecoder } from '../misc';
import { FileSlice, readAscii, readBytes, readI32Be, readU16Be, readU32Be, readU64Be, readU8 } from '../reader';
diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts
index 9db5834..fbcfec4 100644
--- a/src/matroska/matroska-demuxer.ts
+++ b/src/matroska/matroska-demuxer.ts
@@ -30,7 +30,7 @@ import {
InputVideoTrack,
InputVideoTrackBacking,
} from '../input-track';
-import { MediaMetadata } from '../metadata';
+import { MetadataTags } from '../tags';
import { PacketRetrievalOptions } from '../media-sink';
import {
assert,
@@ -94,8 +94,8 @@ type Segment = {
clusters: Cluster[];
clusterLookupMutex: AsyncMutex;
- metadata: MediaMetadata;
- metadataCollected: boolean;
+ metadataTags: MetadataTags;
+ metadataTagsCollected: boolean;
};
type SeekEntry = {
@@ -246,25 +246,25 @@ export class MatroskaDemuxer extends Demuxer {
});
}
- async getMetadata() {
+ async getMetadataTags() {
await this.readMetadata();
- // Load metadata from each segment lazily (only once)
+ // Load metadata tags from each segment lazily (only once)
for (const segment of this.segments) {
- if (!segment.metadataCollected) {
+ if (!segment.metadataTagsCollected) {
await this.loadSegmentMetadata(segment);
- segment.metadataCollected = true;
+ segment.metadataTagsCollected = true;
}
}
// This is kinda handwavy, and how we handle multiple segments isn't suuuuper well-defined anyway; so we just
- // shallow-merge metadata from all (usually just one) segments.
- let metadata: MediaMetadata = {};
+ // shallow-merge metadata tags from all (usually just one) segments.
+ let metadataTags: MetadataTags = {};
for (const segment of this.segments) {
- metadata = { ...metadata, ...segment.metadata };
+ metadataTags = { ...metadataTags, ...segment.metadataTags };
}
- return metadata;
+ return metadataTags;
}
readMetadata() {
@@ -365,8 +365,8 @@ export class MatroskaDemuxer extends Demuxer {
clusters: [],
clusterLookupMutex: new AsyncMutex(),
- metadata: {},
- metadataCollected: false,
+ metadataTags: {},
+ metadataTagsCollected: false,
};
this.segments.push(this.currentSegment);
@@ -1426,8 +1426,8 @@ export class MatroskaDemuxer extends Demuxer {
}
}
- this.currentSegment.metadata.images ??= [];
- this.currentSegment.metadata.images.push({
+ this.currentSegment.metadataTags.images ??= [];
+ this.currentSegment.metadataTags.images.push({
data: this.currentAttachedFile.fileData,
mimeType: this.currentAttachedFile.fileMediaType,
kind,
@@ -1469,50 +1469,50 @@ export class MatroskaDemuxer extends Demuxer {
}
processTagValue(name: string, value: string | Uint8Array) {
- if (!this.currentSegment?.metadata) return;
+ if (!this.currentSegment?.metadataTags) return;
- const metadata = this.currentSegment.metadata;
- metadata.raw ??= {};
- metadata.raw[name] ??= value;
+ const metadataTags = this.currentSegment.metadataTags;
+ metadataTags.raw ??= {};
+ metadataTags.raw[name] ??= value;
if (typeof value === 'string') {
switch (name.toLowerCase()) {
case 'title': {
- metadata.title ??= value;
+ metadataTags.title ??= value;
}; break;
case 'description': {
- metadata.description ??= value;
+ metadataTags.description ??= value;
}; break;
case 'artist': {
- metadata.artist ??= value;
+ metadataTags.artist ??= value;
}; break;
case 'album': {
- metadata.album ??= value;
+ metadataTags.album ??= value;
}; break;
case 'album_artist': {
- metadata.albumArtist ??= value;
+ metadataTags.albumArtist ??= value;
}; break;
case 'genre': {
- metadata.genre ??= value;
+ metadataTags.genre ??= value;
}; break;
case 'comment': {
- metadata.comment ??= value;
+ metadataTags.comment ??= value;
}; break;
case 'lyrics': {
- metadata.lyrics ??= value;
+ metadataTags.lyrics ??= value;
}; break;
case 'date': {
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
- metadata.date ??= date;
+ metadataTags.date ??= date;
}
}; break;
@@ -1523,10 +1523,10 @@ export class MatroskaDemuxer extends Demuxer {
const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
- metadata.trackNumber ??= trackNum;
+ metadataTags.trackNumber ??= trackNum;
}
if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- metadata.trackNumberMax ??= trackNumMax;
+ metadataTags.tracksTotal ??= trackNumMax;
}
}; break;
@@ -1537,10 +1537,10 @@ export class MatroskaDemuxer extends Demuxer {
const discNumMax = discParts[1] && Number.parseInt(discParts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
- metadata.discNumber ??= discNum;
+ metadataTags.discNumber ??= discNum;
}
if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- metadata.discNumberMax ??= discNumMax;
+ metadataTags.discsTotal ??= discNumMax;
}
}; break;
}
diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts
index 60220f2..d878f6e 100644
--- a/src/matroska/matroska-muxer.ts
+++ b/src/matroska/matroska-muxer.ts
@@ -424,10 +424,10 @@ export class MatroskaMuxer extends Muxer {
] });
};
- const metadata = this.output._metadata;
+ const metadataTags = this.output._metadataTags;
const writtenTags = new Set();
- for (const { key, value } of keyValueIterator(metadata)) {
+ for (const { key, value } of keyValueIterator(metadataTags)) {
switch (key) {
case 'title': {
addSimpleTag('TITLE', value);
@@ -475,8 +475,8 @@ export class MatroskaMuxer extends Muxer {
}; break;
case 'trackNumber': {
- const string = metadata.trackNumberMax !== undefined
- ? `${value}/${metadata.trackNumberMax}`
+ const string = metadataTags.tracksTotal !== undefined
+ ? `${value}/${metadataTags.tracksTotal}`
: value.toString();
addSimpleTag('PART_NUMBER', string);
@@ -484,16 +484,16 @@ export class MatroskaMuxer extends Muxer {
}; break;
case 'discNumber': {
- const string = metadata.discNumberMax !== undefined
- ? `${value}/${metadata.discNumberMax}`
+ const string = metadataTags.discsTotal !== undefined
+ ? `${value}/${metadataTags.discsTotal}`
: value.toString();
addSimpleTag('DISC', string);
writtenTags.add('DISC');
}; break;
- case 'trackNumberMax':
- case 'discNumberMax': {
+ case 'tracksTotal':
+ case 'discsTotal': {
// Handled with trackNumber and discNumber respectively
}; break;
@@ -506,9 +506,9 @@ export class MatroskaMuxer extends Muxer {
}
}
- if (metadata.raw) {
- for (const key in metadata.raw) {
- const value = metadata.raw[key]!;
+ if (metadataTags.raw) {
+ for (const key in metadataTags.raw) {
+ const value = metadataTags.raw[key]!;
if (value == null || writtenTags.has(key)) {
continue;
}
@@ -536,14 +536,14 @@ export class MatroskaMuxer extends Muxer {
}
private maybeCreateAttachments() {
- const metadata = this.output._metadata;
- if (!metadata.images || metadata.images.length === 0) {
+ const metadataTags = this.output._metadataTags;
+ if (!metadataTags.images || metadataTags.images.length === 0) {
return;
}
const existingFileUids = new Set();
- this.attachmentsElement = { id: EBMLId.Attachments, data: metadata.images.map((image): EBMLElement => {
+ this.attachmentsElement = { id: EBMLId.Attachments, data: metadataTags.images.map((image): EBMLElement => {
let imageName = image.name;
if (imageName === undefined) {
const baseName = image.kind === 'coverFront' ? 'cover' : image.kind === 'coverBack' ? 'back' : 'image';
diff --git a/src/metadata.ts b/src/metadata.ts
deleted file mode 100644
index 9c37005..0000000
--- a/src/metadata.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-// TODO: Give this a different name. This clashes with a web concept. Question is, what?
-
-export type MediaMetadata = {
- title?: string;
- description?: string;
- artist?: string;
- album?: string;
- albumArtist?: string;
- trackNumber?: number;
- trackNumberMax?: number;
- discNumber?: number;
- discNumberMax?: number;
- genre?: string;
- date?: Date;
- lyrics?: string;
- images?: {
- data: Uint8Array;
- mimeType: string;
- kind: 'coverFront' | 'coverBack' | 'unknown';
- name?: string;
- description?: string;
- }[];
- comment?: string;
- raw?: Record;
-};
-
-export class RichImageData {
- constructor(public data: Uint8Array, public mimeType: string) {}
-}
-
-export const validateMediaMetadata = (metadata: MediaMetadata) => {
- if (!metadata || typeof metadata !== 'object') {
- throw new TypeError('metadata must be an object.');
- }
- if (metadata.title !== undefined && typeof metadata.title !== 'string') {
- throw new TypeError('metadata.title, when provided, must be a string.');
- }
- if (metadata.description !== undefined && typeof metadata.description !== 'string') {
- throw new TypeError('metadata.description, when provided, must be a string.');
- }
- if (metadata.artist !== undefined && typeof metadata.artist !== 'string') {
- throw new TypeError('metadata.artist, when provided, must be a string.');
- }
- if (metadata.album !== undefined && typeof metadata.album !== 'string') {
- throw new TypeError('metadata.album, when provided, must be a string.');
- }
- if (metadata.albumArtist !== undefined && typeof metadata.albumArtist !== 'string') {
- throw new TypeError('metadata.albumArtist, when provided, must be a string.');
- }
- if (metadata.trackNumber !== undefined && (!Number.isInteger(metadata.trackNumber) || metadata.trackNumber <= 0)) {
- throw new TypeError('metadata.trackNumber, when provided, must be a positive integer.');
- }
- if (
- metadata.trackNumberMax !== undefined
- && (!Number.isInteger(metadata.trackNumberMax) || metadata.trackNumberMax <= 0)
- ) {
- throw new TypeError('metadata.trackNumberMax, when provided, must be a positive integer.');
- }
- if (metadata.discNumber !== undefined && (!Number.isInteger(metadata.discNumber) || metadata.discNumber <= 0)) {
- throw new TypeError('metadata.discNumber, when provided, must be a positive integer.');
- }
- if (
- metadata.discNumberMax !== undefined
- && (!Number.isInteger(metadata.discNumberMax) || metadata.discNumberMax <= 0)
- ) {
- throw new TypeError('metadata.discNumberMax, when provided, must be a positive integer.');
- }
- if (metadata.genre !== undefined && typeof metadata.genre !== 'string') {
- throw new TypeError('metadata.genre, when provided, must be a string.');
- }
- if (metadata.date !== undefined && (!(metadata.date instanceof Date) || Number.isNaN(metadata.date.getTime()))) {
- throw new TypeError('metadata.date, when provided, must be a valid Date.');
- }
- if (metadata.lyrics !== undefined && typeof metadata.lyrics !== 'string') {
- throw new TypeError('metadata.lyrics, when provided, must be a string.');
- }
- if (metadata.images !== undefined) {
- if (!Array.isArray(metadata.images)) {
- throw new TypeError('metadata.images, when provided, must be an array.');
- }
- for (const image of metadata.images) {
- if (!image || typeof image !== 'object') {
- throw new TypeError('Each image in metadata.images must be an object.');
- }
- if (!(image.data instanceof Uint8Array)) {
- throw new TypeError('Each image.data must be a Uint8Array.');
- }
- if (typeof image.mimeType !== 'string') {
- throw new TypeError('Each image.mimeType must be a string.');
- }
- if (!['coverFront', 'coverBack', 'other'].includes(image.kind)) {
- throw new TypeError('Each image.kind must be \'coverFront\', \'coverBack\', or \'other\'.');
- }
- }
- }
- if (metadata.comment !== undefined && typeof metadata.comment !== 'string') {
- throw new TypeError('metadata.comment, when provided, must be a string.');
- }
- if (metadata.raw !== undefined) {
- if (!metadata.raw || typeof metadata.raw !== 'object') {
- throw new TypeError('metadata.raw, when provided, must be an object.');
- }
-
- for (const value of Object.values(metadata.raw)) {
- if (
- value !== null
- && typeof value !== 'string'
- && !(value instanceof Uint8Array)
- && !(value instanceof RichImageData)
- ) {
- throw new TypeError(
- 'Each value in metadata.raw must be a string, Uint8Array, RichImageData, or null.',
- );
- }
- }
- }
-};
-
-export const mediaMetadataIsEmpty = (metadata: MediaMetadata) => {
- return metadata.title === undefined
- && metadata.description === undefined
- && metadata.artist === undefined
- && metadata.album === undefined
- && metadata.albumArtist === undefined
- && metadata.trackNumber === undefined
- && metadata.trackNumberMax === undefined
- && metadata.discNumber === undefined
- && metadata.discNumberMax === undefined
- && metadata.genre === undefined
- && metadata.date === undefined
- && metadata.lyrics === undefined
- && (!metadata.images || metadata.images.length === 0)
- && metadata.comment === undefined
- && (metadata.raw === undefined || Object.keys(metadata.raw).length === 0);
-};
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index 432d44f..742a626 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -10,7 +10,7 @@ import { AudioCodec } from '../codec';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
-import { MediaMetadata } from '../metadata';
+import { MetadataTags } from '../tags';
import { PacketRetrievalOptions } from '../media-sink';
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
@@ -38,7 +38,7 @@ export class Mp3Demuxer extends Demuxer {
metadataPromise: Promise | null = null;
firstFrameHeader: FrameHeader | null = null;
loadedSamples: Sample[] = []; // All samples from the start of the file to lastLoadedPos
- metadata: MediaMetadata | null = null;
+ metadataTags: MetadataTags | null = null;
tracks: InputAudioTrack[] = [];
@@ -148,17 +148,17 @@ export class Mp3Demuxer extends Demuxer {
return track.computeDuration();
}
- async getMetadata() {
+ async getMetadataTags() {
const release = await this.readingMutex.acquire();
try {
await this.readMetadata();
- if (this.metadata) {
- return this.metadata;
+ if (this.metadataTags) {
+ return this.metadataTags;
}
- this.metadata = {};
+ this.metadataTags = {};
let currentPos = 0;
let id3V2HeaderFound = false;
@@ -178,7 +178,7 @@ export class Mp3Demuxer extends Demuxer {
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
if (!contentSlice) break;
- parseId3V2Tag(contentSlice, id3V2Header, this.metadata);
+ parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
currentPos = headerSlice.filePos + id3V2Header.size;
}
@@ -191,11 +191,11 @@ export class Mp3Demuxer extends Demuxer {
const tag = readAscii(slice, 3);
if (tag === 'TAG') {
- parseId3V1Tag(slice, this.metadata);
+ parseId3V1Tag(slice, this.metadataTags);
}
}
- return this.metadata;
+ return this.metadataTags;
} finally {
release();
}
diff --git a/src/mp3/mp3-muxer.ts b/src/mp3/mp3-muxer.ts
index 5589a49..a18335b 100644
--- a/src/mp3/mp3-muxer.ts
+++ b/src/mp3/mp3-muxer.ts
@@ -7,7 +7,7 @@
*/
import { assert, assertNever, keyValueIterator, textEncoder, toDataView } from '../misc';
-import { mediaMetadataIsEmpty, MediaMetadata } from '../metadata';
+import { metadataTagsAreEmpty, MetadataTags } from '../tags';
import { Muxer } from '../muxer';
import { Output, OutputAudioTrack } from '../output';
import { Mp3OutputFormat } from '../output-format';
@@ -35,8 +35,8 @@ export class Mp3Muxer extends Muxer {
}
async start() {
- if (!mediaMetadataIsEmpty(this.output._metadata)) {
- this.writeId3v2Tag(this.output._metadata);
+ if (!metadataTagsAreEmpty(this.output._metadataTags)) {
+ this.writeId3v2Tag(this.output._metadataTags);
}
}
@@ -122,7 +122,7 @@ export class Mp3Muxer extends Muxer {
throw new Error('MP3 does not support subtitles.');
}
- writeId3v2Tag(metadata: MediaMetadata) {
+ writeId3v2Tag(tags: MetadataTags) {
this.mp3Writer.writeAscii('ID3');
this.mp3Writer.writeU8(0x04); // Version 2.4
this.mp3Writer.writeU8(0x00); // Revision 0
@@ -132,7 +132,7 @@ export class Mp3Muxer extends Muxer {
const startPos = this.writer.getPos();
const writtenTags = new Set();
- for (const { key, value } of keyValueIterator(metadata)) {
+ for (const { key, value } of keyValueIterator(tags)) {
switch (key) {
case 'title': {
this.mp3Writer.writeId3V2TextFrame('TIT2', value);
@@ -160,8 +160,8 @@ export class Mp3Muxer extends Muxer {
}; break;
case 'trackNumber': {
- const string = metadata.trackNumberMax !== undefined
- ? `${value}/${metadata.trackNumberMax}`
+ const string = tags.tracksTotal !== undefined
+ ? `${value}/${tags.tracksTotal}`
: value.toString();
this.mp3Writer.writeId3V2TextFrame('TRCK', string);
@@ -169,8 +169,8 @@ export class Mp3Muxer extends Muxer {
}; break;
case 'discNumber': {
- const string = metadata.discNumberMax !== undefined
- ? `${value}/${metadata.discNumberMax}`
+ const string = tags.discsTotal !== undefined
+ ? `${value}/${tags.discsTotal}`
: value.toString();
this.mp3Writer.writeId3V2TextFrame('TPOS', string);
@@ -206,8 +206,8 @@ export class Mp3Muxer extends Muxer {
}
}; break;
- case 'trackNumberMax':
- case 'discNumberMax': {
+ case 'tracksTotal':
+ case 'discsTotal': {
// Handled with trackNumber and discNumber respectively
}; break;
@@ -219,9 +219,9 @@ export class Mp3Muxer extends Muxer {
}
}
- if (metadata.raw) {
- for (const key in metadata.raw) {
- const value = metadata.raw[key];
+ if (tags.raw) {
+ for (const key in tags.raw) {
+ const value = tags.raw[key];
if (value == null || key.length !== 4 || writtenTags.has(key)) {
continue;
}
diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts
index 9a6757b..869e702 100644
--- a/src/mp3/mp3-reader.ts
+++ b/src/mp3/mp3-reader.ts
@@ -7,7 +7,7 @@
*/
import { decodeSynchsafe, FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc';
-import { MediaMetadata } from '../metadata';
+import { MetadataTags } from '../tags';
import { coalesceIndex, textDecoder } from '../misc';
import { FileSlice, readAscii, readBytes, Reader, readU32Be, readU8 } from '../reader';
@@ -89,25 +89,25 @@ export const readNextFrameHeader = async (reader: Reader, startPos: number, unti
return null;
};
-export const parseId3V1Tag = (slice: FileSlice, metadata: MediaMetadata) => {
+export const parseId3V1Tag = (slice: FileSlice, tags: MetadataTags) => {
const startPos = slice.filePos;
- metadata.raw ??= {};
- metadata.raw['TAG'] ??= readBytes(slice, ID3_V1_TAG_SIZE - 3); // Dump the whole tag into the raw metadata
+ tags.raw ??= {};
+ tags.raw['TAG'] ??= readBytes(slice, ID3_V1_TAG_SIZE - 3); // Dump the whole tag into the raw metadata
slice.filePos = startPos;
const title = readId3V1String(slice, 30);
- if (title) metadata.title ??= title;
+ if (title) tags.title ??= title;
const artist = readId3V1String(slice, 30);
- if (artist) metadata.artist ??= artist;
+ if (artist) tags.artist ??= artist;
const album = readId3V1String(slice, 30);
- if (album) metadata.album ??= album;
+ if (album) tags.album ??= album;
const yearText = readId3V1String(slice, 4);
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year) && year > 0) {
- metadata.date ??= new Date(year, 0, 1);
+ tags.date ??= new Date(year, 0, 1);
}
const commentBytes = readBytes(slice, 30);
@@ -118,7 +118,7 @@ export const parseId3V1Tag = (slice: FileSlice, metadata: MediaMetadata) => {
if (commentBytes[28] === 0 && commentBytes[29] !== 0) {
const trackNum = commentBytes[29]!;
if (trackNum > 0) {
- metadata.trackNumber ??= trackNum;
+ tags.trackNumber ??= trackNum;
}
slice.skip(-30);
@@ -129,11 +129,11 @@ export const parseId3V1Tag = (slice: FileSlice, metadata: MediaMetadata) => {
comment = readId3V1String(slice, 30);
}
- if (comment) metadata.comment ??= comment;
+ if (comment) tags.comment ??= comment;
const genreIndex = readU8(slice);
if (genreIndex < ID3_V1_GENRES.length) {
- metadata.genre ??= ID3_V1_GENRES[genreIndex];
+ tags.genre ??= ID3_V1_GENRES[genreIndex];
}
};
@@ -171,7 +171,7 @@ export const readId3V2Header = (slice: FileSlice): Id3V2Header | null => {
return { majorVersion, revision, flags, size };
};
-export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: MediaMetadata) => {
+export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: MetadataTags) => {
// https://id3.org/id3v2.3.0
if (![2, 3, 4].includes(header.majorVersion)) {
@@ -239,13 +239,13 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
reader.ununsynchronizeRegion(reader.pos, frameEndPos);
}
- metadata.raw ??= {};
+ tags.raw ??= {};
if (frame.id[0] === 'T') {
// It's a text frame, let's decode as text
- metadata.raw[frame.id] ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.raw[frame.id] ??= reader.readId3V2EncodingAndText(frameEndPos);
} else {
// For the others, let's just get the bytes
- metadata.raw[frame.id] ??= reader.readBytes(frame.size);
+ tags.raw[frame.id] ??= reader.readBytes(frame.size);
}
reader.pos = frameStartPos;
@@ -253,27 +253,27 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
switch (frame.id) {
case 'TIT2':
case 'TT2': {
- metadata.title ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.title ??= reader.readId3V2EncodingAndText(frameEndPos);
}; break;
case 'TIT3':
case 'TT3': {
- metadata.description ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.description ??= reader.readId3V2EncodingAndText(frameEndPos);
}; break;
case 'TPE1':
case 'TP1': {
- metadata.artist ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.artist ??= reader.readId3V2EncodingAndText(frameEndPos);
}; break;
case 'TALB':
case 'TAL': {
- metadata.album ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.album ??= reader.readId3V2EncodingAndText(frameEndPos);
}; break;
case 'TPE2':
case 'TP2': {
- metadata.albumArtist ??= reader.readId3V2EncodingAndText(frameEndPos);
+ tags.albumArtist ??= reader.readId3V2EncodingAndText(frameEndPos);
}; break;
case 'TRCK':
@@ -284,10 +284,10 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
- metadata.trackNumber ??= trackNum;
+ tags.trackNumber ??= trackNum;
}
if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- metadata.trackNumberMax ??= trackNumMax;
+ tags.tracksTotal ??= trackNumMax;
}
}; break;
@@ -299,10 +299,10 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const discNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
- metadata.discNumber ??= discNum;
+ tags.discNumber ??= discNum;
}
if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- metadata.discNumberMax ??= discNumMax;
+ tags.discsTotal ??= discNumMax;
}
}; break;
@@ -313,7 +313,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
if (match) {
const genreNumber = Number.parseInt(match[1]!);
if (ID3_V1_GENRES[genreNumber] !== undefined) {
- metadata.genre ??= ID3_V1_GENRES[genreNumber];
+ tags.genre ??= ID3_V1_GENRES[genreNumber];
break;
}
}
@@ -322,12 +322,12 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
if (match) {
const genreNumber = Number.parseInt(match[0]);
if (ID3_V1_GENRES[genreNumber] !== undefined) {
- metadata.genre ??= ID3_V1_GENRES[genreNumber];
+ tags.genre ??= ID3_V1_GENRES[genreNumber];
break;
}
}
- metadata.genre ??= genreText;
+ tags.genre ??= genreText;
}; break;
case 'TDRC':
@@ -336,7 +336,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const date = new Date(dateText);
if (!Number.isNaN(date.getTime())) {
- metadata.date ??= date;
+ tags.date ??= date;
}
}; break;
@@ -346,7 +346,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const year = Number.parseInt(yearText, 10);
if (Number.isInteger(year)) {
- metadata.date ??= new Date(year, 0, 1);
+ tags.date ??= new Date(year, 0, 1);
}
}; break;
@@ -355,7 +355,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const encoding = reader.readU8();
reader.pos += 3; // Skip language
reader.readId3V2Text(encoding, frameEndPos); // Short content description
- metadata.lyrics ??= reader.readId3V2Text(encoding, frameEndPos);
+ tags.lyrics ??= reader.readId3V2Text(encoding, frameEndPos);
}; break;
case 'COMM':
@@ -363,7 +363,7 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
const encoding = reader.readU8();
reader.pos += 3; // Skip language
reader.readId3V2Text(encoding, frameEndPos); // Short content description
- metadata.comment ??= reader.readId3V2Text(encoding, frameEndPos);
+ tags.comment ??= reader.readId3V2Text(encoding, frameEndPos);
}; break;
case 'APIC':
@@ -389,8 +389,8 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, metadata: M
if (imageDataSize >= 0) {
const imageData = reader.readBytes(imageDataSize);
- if (!metadata.images) metadata.images = [];
- metadata.images.push({
+ if (!tags.images) tags.images = [];
+ tags.images.push({
data: imageData,
mimeType,
kind: pictureType === 3
diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts
index 4fb4c54..88e248b 100644
--- a/src/ogg/ogg-demuxer.ts
+++ b/src/ogg/ogg-demuxer.ts
@@ -12,7 +12,7 @@ import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
-import { MediaMetadata } from '../metadata';
+import { MetadataTags } from '../tags';
import {
assert,
AsyncMutex,
@@ -60,7 +60,7 @@ export class OggDemuxer extends Demuxer {
metadataPromise: Promise | null = null;
bitstreams: LogicalBitstream[] = [];
tracks: InputAudioTrack[] = [];
- metadata: MediaMetadata = {};
+ metadataTags: MetadataTags = {};
constructor(input: Input) {
super(input);
@@ -274,8 +274,8 @@ export class OggDemuxer extends Demuxer {
if (vendorStringLength > 0) {
// Expose the vendor string in the raw metadata
- this.metadata.raw ??= {};
- this.metadata.raw['vendor'] ??= vendorString;
+ this.metadataTags.raw ??= {};
+ this.metadataTags.raw['vendor'] ??= vendorString;
}
const listLength = commentView.getUint32(commentPos, true);
@@ -299,36 +299,36 @@ export class OggDemuxer extends Demuxer {
const key = string.slice(0, separatorIndex).toUpperCase();
const value = string.slice(separatorIndex + 1);
- this.metadata.raw ??= {};
- this.metadata.raw[key] ??= value;
+ this.metadataTags.raw ??= {};
+ this.metadataTags.raw[key] ??= value;
switch (key) {
case 'TITLE': {
- this.metadata.title ??= value;
+ this.metadataTags.title ??= value;
}; break;
case 'DESCRIPTION': {
- this.metadata.description ??= value;
+ this.metadataTags.description ??= value;
}; break;
case 'ARTIST': {
- this.metadata.artist ??= value;
+ this.metadataTags.artist ??= value;
}; break;
case 'ALBUM': {
- this.metadata.album ??= value;
+ this.metadataTags.album ??= value;
}; break;
case 'ALBUMARTIST': {
- this.metadata.albumArtist ??= value;
+ this.metadataTags.albumArtist ??= value;
}; break;
case 'COMMENT': {
- this.metadata.comment ??= value;
+ this.metadataTags.comment ??= value;
}; break;
case 'LYRICS': {
- this.metadata.lyrics ??= value;
+ this.metadataTags.lyrics ??= value;
}; break;
case 'TRACKNUMBER': {
@@ -337,17 +337,17 @@ export class OggDemuxer extends Demuxer {
const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
- this.metadata.trackNumber ??= trackNum;
+ this.metadataTags.trackNumber ??= trackNum;
}
if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadata.trackNumberMax ??= trackNumMax;
+ this.metadataTags.tracksTotal ??= trackNumMax;
}
}; break;
case 'TRACKTOTAL': {
const trackNumMax = Number.parseInt(value, 10);
if (Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadata.trackNumberMax ??= trackNumMax;
+ this.metadataTags.tracksTotal ??= trackNumMax;
}
}; break;
@@ -357,29 +357,29 @@ export class OggDemuxer extends Demuxer {
const discNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(discNum) && discNum > 0) {
- this.metadata.discNumber ??= discNum;
+ this.metadataTags.discNumber ??= discNum;
}
if (discNumMax && Number.isInteger(discNumMax) && discNumMax > 0) {
- this.metadata.discNumberMax ??= discNumMax;
+ this.metadataTags.discsTotal ??= discNumMax;
}
}; break;
case 'DISCTOTAL': {
const trackNumMax = Number.parseInt(value, 10);
if (Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadata.trackNumberMax ??= trackNumMax;
+ this.metadataTags.tracksTotal ??= trackNumMax;
}
}; break;
case 'DATE': {
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
- this.metadata.date ??= date;
+ this.metadataTags.date ??= date;
}
}; break;
case 'GENRE': {
- this.metadata.genre ??= value;
+ this.metadataTags.genre ??= value;
}; break;
case 'METADATA_BLOCK_PICTURE': {
@@ -401,8 +401,8 @@ export class OggDemuxer extends Demuxer {
mediaTypeLength + descriptionLength + 32 + dataLength,
);
- this.metadata.images ??= [];
- this.metadata.images.push({
+ this.metadataTags.images ??= [];
+ this.metadataTags.images.push({
data,
mimeType: mediaType,
kind: pictureType === 3 ? 'coverFront' : pictureType === 4 ? 'coverBack' : 'unknown',
@@ -551,9 +551,9 @@ export class OggDemuxer extends Demuxer {
return Math.max(0, ...trackDurations);
}
- async getMetadata() {
+ async getMetadataTags() {
await this.readMetadata();
- return this.metadata;
+ return this.metadataTags;
}
}
diff --git a/src/ogg/ogg-muxer.ts b/src/ogg/ogg-muxer.ts
index 12a8a06..8e2cc5e 100644
--- a/src/ogg/ogg-muxer.ts
+++ b/src/ogg/ogg-muxer.ts
@@ -262,14 +262,14 @@ export class OggMuxer extends Muxer {
createVorbisComments(headerBytes: Uint8Array) {
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.2
- const metadata = this.output._metadata;
+ const tags = this.output._metadataTags;
const commentHeaderParts = [
headerBytes,
];
let vendorString = '';
- if (typeof metadata.raw?.['vendor'] === 'string') {
- vendorString = metadata.raw?.['vendor'];
+ if (typeof tags.raw?.['vendor'] === 'string') {
+ vendorString = tags.raw?.['vendor'];
}
const encodedVendorString = textEncoder.encode(vendorString);
@@ -295,7 +295,7 @@ export class OggMuxer extends Muxer {
writtenTags.add(key);
};
- for (const { key, value } of keyValueIterator(metadata)) {
+ for (const { key, value } of keyValueIterator(tags)) {
switch (key) {
case 'title': {
addCommentTag('TITLE', value);
@@ -333,22 +333,22 @@ export class OggMuxer extends Muxer {
addCommentTag('LYRICS', value);
}; break;
- case 'discNumber': {
- addCommentTag('TRACKNUMBER', value.toString());
- }; break;
-
- case 'discNumberMax': {
- addCommentTag('TRACKTOTAL', value.toString());
- }; break;
-
case 'trackNumber': {
addCommentTag('DISCNUMBER', value.toString());
}; break;
- case 'trackNumberMax': {
+ case 'tracksTotal': {
addCommentTag('DISCTOTAL', value.toString());
}; break;
+ case 'discNumber': {
+ addCommentTag('TRACKNUMBER', value.toString());
+ }; break;
+
+ case 'discsTotal': {
+ addCommentTag('TRACKTOTAL', value.toString());
+ }; break;
+
case 'images': {
for (const image of value) {
// https://datatracker.ietf.org/doc/rfc9639/ Section 8.8
@@ -400,9 +400,9 @@ export class OggMuxer extends Muxer {
}
}
- if (metadata.raw) {
- for (const key in metadata.raw) {
- const value = metadata.raw[key];
+ if (tags.raw) {
+ for (const key in tags.raw) {
+ const value = tags.raw[key];
if (key === 'vendor' || value == null || writtenTags.has(key)) {
continue;
}
diff --git a/src/output.ts b/src/output.ts
index 234941f..e1ff034 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -7,7 +7,7 @@
*/
import { AsyncMutex, isIso639Dash2LanguageCode, Rotation } from './misc';
-import { MediaMetadata, validateMediaMetadata } from './metadata';
+import { MetadataTags, validateMetadataTags } from './tags';
import { Muxer } from './muxer';
import { OutputFormat } from './output-format';
import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './media-source';
@@ -147,7 +147,7 @@ export class Output<
/** @internal */
_mutex = new AsyncMutex();
/** @internal */
- _metadata: MediaMetadata = {};
+ _metadataTags: MetadataTags = {};
/**
* Creates a new instance of {@link Output} which can then be used to create a new media file according to the
@@ -220,14 +220,14 @@ export class Output<
this._addTrack('subtitle', source, metadata);
}
- setMetadata(metadata: MediaMetadata) {
- validateMediaMetadata(metadata);
+ setMetadataTags(tags: MetadataTags) {
+ validateMetadataTags(tags);
if (this.state !== 'pending') {
- throw new Error('Cannot set metadata after output has been started or canceled.');
+ throw new Error('Cannot set metadata tags after output has been started or canceled.');
}
- this._metadata = metadata;
+ this._metadataTags = tags;
}
/** @internal */
diff --git a/src/tags.ts b/src/tags.ts
new file mode 100644
index 0000000..68b6947
--- /dev/null
+++ b/src/tags.ts
@@ -0,0 +1,133 @@
+export type MetadataTags = {
+ title?: string;
+ description?: string;
+ artist?: string;
+ album?: string;
+ albumArtist?: string;
+ trackNumber?: number;
+ tracksTotal?: number;
+ discNumber?: number;
+ discsTotal?: number;
+ genre?: string;
+ date?: Date;
+ lyrics?: string;
+ comment?: string;
+ images?: {
+ data: Uint8Array;
+ mimeType: string;
+ kind: 'coverFront' | 'coverBack' | 'unknown';
+ name?: string;
+ description?: string;
+ }[];
+ raw?: Record;
+};
+
+export class RichImageData {
+ constructor(public data: Uint8Array, public mimeType: string) {}
+}
+
+export const validateMetadataTags = (tags: MetadataTags) => {
+ if (!tags || typeof tags !== 'object') {
+ throw new TypeError('tags must be an object.');
+ }
+ if (tags.title !== undefined && typeof tags.title !== 'string') {
+ throw new TypeError('tags.title, when provided, must be a string.');
+ }
+ if (tags.description !== undefined && typeof tags.description !== 'string') {
+ throw new TypeError('tags.description, when provided, must be a string.');
+ }
+ if (tags.artist !== undefined && typeof tags.artist !== 'string') {
+ throw new TypeError('tags.artist, when provided, must be a string.');
+ }
+ if (tags.album !== undefined && typeof tags.album !== 'string') {
+ throw new TypeError('tags.album, when provided, must be a string.');
+ }
+ if (tags.albumArtist !== undefined && typeof tags.albumArtist !== 'string') {
+ throw new TypeError('tags.albumArtist, when provided, must be a string.');
+ }
+ if (tags.trackNumber !== undefined && (!Number.isInteger(tags.trackNumber) || tags.trackNumber <= 0)) {
+ throw new TypeError('tags.trackNumber, when provided, must be a positive integer.');
+ }
+ if (
+ tags.tracksTotal !== undefined
+ && (!Number.isInteger(tags.tracksTotal) || tags.tracksTotal <= 0)
+ ) {
+ throw new TypeError('tags.tracksTotal, when provided, must be a positive integer.');
+ }
+ if (tags.discNumber !== undefined && (!Number.isInteger(tags.discNumber) || tags.discNumber <= 0)) {
+ throw new TypeError('tags.discNumber, when provided, must be a positive integer.');
+ }
+ if (
+ tags.discsTotal !== undefined
+ && (!Number.isInteger(tags.discsTotal) || tags.discsTotal <= 0)
+ ) {
+ throw new TypeError('tags.discsTotal, when provided, must be a positive integer.');
+ }
+ if (tags.genre !== undefined && typeof tags.genre !== 'string') {
+ throw new TypeError('tags.genre, when provided, must be a string.');
+ }
+ if (tags.date !== undefined && (!(tags.date instanceof Date) || Number.isNaN(tags.date.getTime()))) {
+ throw new TypeError('tags.date, when provided, must be a valid Date.');
+ }
+ if (tags.lyrics !== undefined && typeof tags.lyrics !== 'string') {
+ throw new TypeError('tags.lyrics, when provided, must be a string.');
+ }
+ if (tags.images !== undefined) {
+ if (!Array.isArray(tags.images)) {
+ throw new TypeError('tags.images, when provided, must be an array.');
+ }
+ for (const image of tags.images) {
+ if (!image || typeof image !== 'object') {
+ throw new TypeError('Each image in tags.images must be an object.');
+ }
+ if (!(image.data instanceof Uint8Array)) {
+ throw new TypeError('Each image.data must be a Uint8Array.');
+ }
+ if (typeof image.mimeType !== 'string') {
+ throw new TypeError('Each image.mimeType must be a string.');
+ }
+ if (!['coverFront', 'coverBack', 'other'].includes(image.kind)) {
+ throw new TypeError('Each image.kind must be \'coverFront\', \'coverBack\', or \'other\'.');
+ }
+ }
+ }
+ if (tags.comment !== undefined && typeof tags.comment !== 'string') {
+ throw new TypeError('tags.comment, when provided, must be a string.');
+ }
+ if (tags.raw !== undefined) {
+ if (!tags.raw || typeof tags.raw !== 'object') {
+ throw new TypeError('tags.raw, when provided, must be an object.');
+ }
+
+ for (const value of Object.values(tags.raw)) {
+ if (
+ value !== null
+ && typeof value !== 'string'
+ && !(value instanceof Uint8Array)
+ && !(value instanceof RichImageData)
+ ) {
+ throw new TypeError(
+ 'Each value in tags.raw must be a string, Uint8Array, RichImageData, or null.',
+ );
+ }
+ }
+ }
+};
+
+export const metadataTagsAreEmpty = (tags: MetadataTags) => {
+ return tags.title === undefined
+ && tags.description === undefined
+ && tags.artist === undefined
+ && tags.album === undefined
+ && tags.albumArtist === undefined
+ && tags.trackNumber === undefined
+ && tags.tracksTotal === undefined
+ && tags.discNumber === undefined
+ && tags.discsTotal === undefined
+ && tags.genre === undefined
+ && tags.date === undefined
+ && tags.lyrics === undefined
+ && (!tags.images || tags.images.length === 0)
+ && tags.comment === undefined
+ && (tags.raw === undefined || Object.keys(tags.raw).length === 0);
+};
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index 6f4799e..0c7ef97 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -11,7 +11,7 @@ import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
-import { MediaMetadata } from '../metadata';
+import { MetadataTags } from '../tags';
import { assert, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { readAscii, readBytes, Reader, readU16, readU32, readU64 } from '../reader';
@@ -40,7 +40,7 @@ export class WaveDemuxer extends Demuxer {
tracks: InputAudioTrack[] = [];
lastKnownPacketIndex = 0;
- metadata: MediaMetadata = {};
+ metadataTags: MetadataTags = {};
constructor(input: Input) {
super(input);
@@ -204,25 +204,25 @@ export class WaveDemuxer extends Demuxer {
const value = String.fromCharCode(...bytes.subarray(0, stringLength));
- this.metadata.raw ??= {};
- this.metadata.raw[chunkName] = value;
+ this.metadataTags.raw ??= {};
+ this.metadataTags.raw[chunkName] = value;
switch (chunkName) {
case 'INAM':
case 'TITL': {
- this.metadata.title ??= value;
+ this.metadataTags.title ??= value;
}; break;
case 'TIT3': {
- this.metadata.description ??= value;
+ this.metadataTags.description ??= value;
}; break;
case 'IART': {
- this.metadata.artist ??= value;
+ this.metadataTags.artist ??= value;
}; break;
case 'IPRD': {
- this.metadata.album ??= value;
+ this.metadataTags.album ??= value;
}; break;
case 'IPRT':
@@ -233,10 +233,10 @@ export class WaveDemuxer extends Demuxer {
const trackNumMax = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
- this.metadata.trackNumber ??= trackNum;
+ this.metadataTags.trackNumber ??= trackNum;
}
if (trackNumMax && Number.isInteger(trackNumMax) && trackNumMax > 0) {
- this.metadata.trackNumberMax ??= trackNumMax;
+ this.metadataTags.tracksTotal ??= trackNumMax;
}
}; break;
@@ -244,26 +244,26 @@ export class WaveDemuxer extends Demuxer {
case 'IDIT': {
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
- this.metadata.date ??= date;
+ this.metadataTags.date ??= date;
}
}; break;
case 'YEAR': {
const year = Number.parseInt(value, 10);
if (Number.isInteger(year) && year > 0) {
- this.metadata.date ??= new Date(year, 0, 1);
+ this.metadataTags.date ??= new Date(year, 0, 1);
}
}; break;
case 'IGNR':
case 'GENR': {
- this.metadata.genre ??= value;
+ this.metadataTags.genre ??= value;
}; break;
case 'ICMT':
case 'CMNT':
case 'COMM': {
- this.metadata.comment ??= value;
+ this.metadataTags.comment ??= value;
}; break;
}
@@ -319,9 +319,9 @@ export class WaveDemuxer extends Demuxer {
return this.tracks;
}
- async getMetadata() {
+ async getMetadataTags() {
await this.readMetadata();
- return this.metadata;
+ return this.metadataTags;
}
}
diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts
index 33efd3f..c8126da 100644
--- a/src/wave/wave-muxer.ts
+++ b/src/wave/wave-muxer.ts
@@ -15,7 +15,7 @@ import { Writer } from '../writer';
import { EncodedPacket } from '../packet';
import { WavOutputFormat } from '../output-format';
import { assert, assertNever, keyValueIterator } from '../misc';
-import { MediaMetadata, mediaMetadataIsEmpty } from '../metadata';
+import { MetadataTags, metadataTagsAreEmpty } from '../tags';
export class WaveMuxer extends Muxer {
private format: WavOutputFormat;
@@ -159,9 +159,9 @@ export class WaveMuxer extends Muxer {
this.riffWriter.writeU16(blockSize);
this.riffWriter.writeU16(8 * pcmInfo.sampleSize);
- if (!mediaMetadataIsEmpty(this.output._metadata)) {
+ if (!metadataTagsAreEmpty(this.output._metadataTags)) {
// Metadata exists, let's write an INFO chunk
- this.writeInfoChunk(this.output._metadata);
+ this.writeInfoChunk(this.output._metadataTags);
}
// data chunk
@@ -180,7 +180,7 @@ export class WaveMuxer extends Muxer {
}
}
- private writeInfoChunk(metadata: MediaMetadata) {
+ private writeInfoChunk(metadata: MetadataTags) {
const startPos = this.writer.getPos();
this.riffWriter.writeAscii('LIST');
@@ -207,8 +207,8 @@ export class WaveMuxer extends Muxer {
}; break;
case 'trackNumber': {
- const string = metadata.trackNumberMax !== undefined
- ? `${value}/${metadata.trackNumberMax}`
+ const string = metadata.tracksTotal !== undefined
+ ? `${value}/${metadata.tracksTotal}`
: value.toString();
this.writeInfoTag('ITRK', string);
@@ -232,8 +232,8 @@ export class WaveMuxer extends Muxer {
case 'albumArtist':
case 'discNumber':
- case 'trackNumberMax':
- case 'discNumberMax':
+ case 'tracksTotal':
+ case 'discsTotal':
case 'description':
case 'lyrics':
case 'images': {