@stacksjs/audio 0.70.163
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/LICENSE.md +21 -0
- package/README.md +66 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +173 -0
- package/package.json +41 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @stacksjs/audio
|
|
2
|
+
|
|
3
|
+
Capability-aware audio planning, format negotiation, waveforms, and transcript normalization for Stacks.
|
|
4
|
+
|
|
5
|
+
## Process music, podcasts, and voice
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { detectAudioRuntimeCapabilities } from '@ts-audio/core'
|
|
9
|
+
import { audio } from '@stacksjs/audio'
|
|
10
|
+
|
|
11
|
+
const derivatives = await audio('uploads/episode.flac')
|
|
12
|
+
.profile({
|
|
13
|
+
codec: 'flac',
|
|
14
|
+
container: 'flac',
|
|
15
|
+
duration: 1842,
|
|
16
|
+
sampleRate: 48_000,
|
|
17
|
+
channels: 2,
|
|
18
|
+
})
|
|
19
|
+
.content('speech')
|
|
20
|
+
.output(['opus', 'aac', 'mp3'])
|
|
21
|
+
.loudness(-16)
|
|
22
|
+
.runtime(await detectAudioRuntimeCapabilities())
|
|
23
|
+
.process({ signal: request.signal })
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `.content('music')` for music bitrate guidance, `.content('speech')` for podcasts and voice, or `.content('general')` for mixed material. AAC output uses a native ADTS `.aac` container, Opus uses Ogg, and MP3 uses its native container. `.generate()` can inspect the plan without processing it.
|
|
27
|
+
|
|
28
|
+
## Negotiation, waveform, and transcripts
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createWaveform, negotiateAudioOutput, normalizeTranscript } from '@stacksjs/audio'
|
|
32
|
+
|
|
33
|
+
const selected = negotiateAudioOutput(plan.outputs, request.headers.get('accept') ?? '*/*')
|
|
34
|
+
const waveform = createWaveform(decodedChannels, sampleRate, 1200)
|
|
35
|
+
const transcript = normalizeTranscript('en', providerSegments)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Transcription remains provider-neutral. Call the provider explicitly, bind its cache key to the source hash, language, provider version, and options, then store the normalized WebVTT beside the private media.
|
|
39
|
+
|
|
40
|
+
## HLS, private CDN, and DRM
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createProtectedAudioPlaylist } from '@stacksjs/audio'
|
|
44
|
+
|
|
45
|
+
const delivery = await createProtectedAudioPlaylist(segments, {
|
|
46
|
+
key,
|
|
47
|
+
keyUri: '/media/keys/episode',
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Native HLS AES-128 uses Web Crypto. Proprietary DRM metadata is accepted only for already encrypted media. Publish immutable audio segments to S3 and protect manifests, keys, waveform data, transcripts, and metadata with the same CloudFront signed URL, signed cookie, or application authorization boundary.
|
|
52
|
+
|
|
53
|
+
## Default STX player
|
|
54
|
+
|
|
55
|
+
```stx
|
|
56
|
+
<Audio
|
|
57
|
+
src="/media/episode/index.m3u8"
|
|
58
|
+
title="Episode 12"
|
|
59
|
+
artist="Stacks"
|
|
60
|
+
waveform="/media/episode/waveform.svg"
|
|
61
|
+
:sources="progressiveFallbacks"
|
|
62
|
+
:tracks="chapterAndTranscriptTracks"
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The default UI includes play, seek, time, mute, volume, speed, settings, waveform seeking, AirPlay, and Remote Playback. Native audio controls remain available before custom elements load and when JavaScript is unavailable.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare function recommendAudioBitrate(format: AudioFormat, source: AudioProfile, content: AudioContent): number;
|
|
2
|
+
export declare function audio(source: string): AudioBuilder;
|
|
3
|
+
export declare function processAudioPlan(plan: AudioPlan, options?: AudioProcessOptions): Promise<ProcessedAudioDerivative[]>;
|
|
4
|
+
export declare function assertAudioPlanExecutable(plan: AudioPlan): void;
|
|
5
|
+
export declare function negotiateAudioOutput(outputs: readonly AudioOutput[], accept?: string): AudioOutput | undefined;
|
|
6
|
+
export declare function createWaveform(channels: readonly Float32Array[], sampleRate: number, samples?: number, precision?: number): Waveform;
|
|
7
|
+
export declare function normalizeTranscript(language: string, input: readonly TranscriptSegment[]): Transcript;
|
|
8
|
+
export declare function audioResponseHeaders(bytes: number, etag: string, contentType: string): Record<string, string>;
|
|
9
|
+
export declare function signAudioAsset(path: string, expires: number, secret: string): string;
|
|
10
|
+
export declare function verifyAudioAsset(path: string, expires: number, signature: string, secret: string, now?: number): boolean;
|
|
11
|
+
export declare function createProtectedAudioPlaylist(segments: AudioSegment[], protection?: AudioHlsProtection): Promise<{ playlist: string, files: Record<string, Uint8Array>, encrypted: boolean }>;
|
|
12
|
+
export declare interface AudioProfile { codec: AudioCodec, container: string, duration: number, sampleRate: number, channels: number, bitrate?: number }
|
|
13
|
+
export declare interface AudioCapabilities { encoder: boolean, codecs: AudioFormat[] }
|
|
14
|
+
export declare interface AudioOutput { format: AudioFormat, container: 'ogg' | 'aac' | 'mp3', mimeType: 'audio/ogg; codecs=opus' | 'audio/aac' | 'audio/mpeg', extension: 'ogg' | 'aac' | 'mp3', bitrate: number, action: 'copy' | 'transcode', available: boolean, reason?: string }
|
|
15
|
+
export declare interface AudioPlan { source: string, profile: AudioProfile, outputs: AudioOutput[], loudness: number }
|
|
16
|
+
export declare interface AudioProcessOptions { batchSize?: number, signal?: AbortSignal }
|
|
17
|
+
export declare interface ProcessedAudioDerivative { output: AudioOutput, bytes: Uint8Array }
|
|
18
|
+
export declare interface Waveform { version: 1, channels: number, sampleRate: number, duration: number, samplesPerPeak: number, peaks: number[][] }
|
|
19
|
+
export declare interface TranscriptSegment { startTime: number, endTime: number, text: string, confidence?: number, speaker?: string }
|
|
20
|
+
export declare interface Transcript { language: string, segments: Array<TranscriptSegment & { id: number }>, vtt: string }
|
|
21
|
+
export declare interface AudioSegment { uri: string, duration: number, data: Uint8Array }
|
|
22
|
+
export declare interface AudioHlsProtection { key: Uint8Array, keyUri: string }
|
|
23
|
+
export type AudioFormat = 'opus' | 'aac' | 'mp3';
|
|
24
|
+
export type AudioContent = 'speech' | 'music' | 'general';
|
|
25
|
+
export type AudioCodec = 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac' | 'alac' | 'pcm_s16le' | 'pcm_s24le' | 'pcm_s32le' | 'pcm_f32le' | 'pcm_f64le';
|
|
26
|
+
export declare class AudioBuilder {
|
|
27
|
+
constructor(source: string);
|
|
28
|
+
profile(value: AudioProfile): this;
|
|
29
|
+
output(value: AudioFormat[]): this;
|
|
30
|
+
content(value: AudioContent): this;
|
|
31
|
+
loudness(value: number): this;
|
|
32
|
+
runtime(value: AudioCapabilities): this;
|
|
33
|
+
generate(): AudioPlan;
|
|
34
|
+
process(options?: AudioProcessOptions): Promise<ProcessedAudioDerivative[]>;
|
|
35
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const details = { opus: { container: "ogg", mimeType: "audio/ogg; codecs=opus", extension: "ogg" }, aac: { container: "aac", mimeType: "audio/aac", extension: "aac" }, mp3: { container: "mp3", mimeType: "audio/mpeg", extension: "mp3" } };
|
|
3
|
+
function validate(profile) {
|
|
4
|
+
for (const [name, value] of Object.entries({ duration: profile.duration, sampleRate: profile.sampleRate, channels: profile.channels }))
|
|
5
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
6
|
+
throw TypeError(`Audio ${name} must be positive`);
|
|
7
|
+
}
|
|
8
|
+
export function recommendAudioBitrate(format, source, content) {
|
|
9
|
+
const channels = Math.min(2, Math.max(1, source.channels)), rate = source.sampleRate <= 24000 ? 0.75 : source.sampleRate >= 88200 ? 1.15 : 1, base = content === "speech" ? channels === 1 ? 48000 : 64000 : content === "music" ? channels === 1 ? 96000 : 192000 : channels === 1 ? 64000 : 128000;
|
|
10
|
+
return Math.max(32000, Math.min(source.bitrate ?? 1 / 0, Math.round(base * rate * (format === "opus" ? 0.8 : format === "mp3" ? 1.2 : 1) / 1000) * 1000));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class AudioBuilder {
|
|
14
|
+
source;
|
|
15
|
+
inspected;
|
|
16
|
+
formats = ["opus", "aac", "mp3"];
|
|
17
|
+
contentType = "general";
|
|
18
|
+
targetLoudness = -16;
|
|
19
|
+
capabilities = { encoder: !1, codecs: [] };
|
|
20
|
+
constructor(source) {
|
|
21
|
+
this.source = source;
|
|
22
|
+
}
|
|
23
|
+
profile(value) {
|
|
24
|
+
validate(value);
|
|
25
|
+
this.inspected = value;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
output(value) {
|
|
29
|
+
if (!value.length)
|
|
30
|
+
throw TypeError("Audio formats are required");
|
|
31
|
+
this.formats = [...new Set(value)];
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
content(value) {
|
|
35
|
+
this.contentType = value;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
loudness(value) {
|
|
39
|
+
if (!Number.isFinite(value) || value < -70 || value > 0)
|
|
40
|
+
throw TypeError("Audio loudness must be between -70 and 0 LUFS");
|
|
41
|
+
this.targetLoudness = value;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
runtime(value) {
|
|
45
|
+
this.capabilities = value;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
generate() {
|
|
49
|
+
if (!this.inspected)
|
|
50
|
+
throw Error("Audio inspection is required; pass its profile with .profile()");
|
|
51
|
+
const profile = this.inspected, outputs = this.formats.map((format) => {
|
|
52
|
+
const info = details[format], copy = profile.codec === format && (profile.container === info.container || profile.container === info.extension), available = copy || this.capabilities.encoder && this.capabilities.codecs.includes(format);
|
|
53
|
+
return { format, ...info, bitrate: recommendAudioBitrate(format, profile, this.contentType), action: copy ? "copy" : "transcode", available, reason: available ? void 0 : `Native ${format} encoding is unavailable` };
|
|
54
|
+
});
|
|
55
|
+
return { source: this.source, profile, outputs, loudness: this.targetLoudness };
|
|
56
|
+
}
|
|
57
|
+
async process(options = {}) {
|
|
58
|
+
return processAudioPlan(this.generate(), options);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function audio(source) {
|
|
62
|
+
return new AudioBuilder(source);
|
|
63
|
+
}
|
|
64
|
+
export async function processAudioPlan(plan, options = {}) {
|
|
65
|
+
assertAudioPlanExecutable(plan);
|
|
66
|
+
const { generateAudioDerivatives } = await import("@ts-audio/core/native-transcode");
|
|
67
|
+
return generateAudioDerivatives(plan.source, { source: plan.profile, outputs: plan.outputs }, options);
|
|
68
|
+
}
|
|
69
|
+
export function assertAudioPlanExecutable(plan) {
|
|
70
|
+
const missing = plan.outputs.filter((value) => !value.available);
|
|
71
|
+
if (missing.length)
|
|
72
|
+
throw Error(missing.map((value) => `${value.format}: ${value.reason}`).join("; "));
|
|
73
|
+
}
|
|
74
|
+
function q(accept, mime) {
|
|
75
|
+
const [type = "", subtype = ""] = (mime.split(";", 1)[0] ?? "").split("/");
|
|
76
|
+
let best = 0;
|
|
77
|
+
for (const item of accept.split(",")) {
|
|
78
|
+
const [range = "*/*", ...params] = item.trim().toLowerCase().split(";").map((value) => value.trim()), [a = "*", b = "*"] = range.split("/");
|
|
79
|
+
if ((a === "*" || a === type) && (b === "*" || b === subtype)) {
|
|
80
|
+
const raw = params.find((value) => value.startsWith("q="));
|
|
81
|
+
best = Math.max(best, raw ? Number.parseFloat(raw.slice(2)) || 0 : 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
85
|
+
}
|
|
86
|
+
export function negotiateAudioOutput(outputs, accept = "*/*") {
|
|
87
|
+
return outputs.filter((value) => value.available).map((output, index) => ({ output, index, q: q(accept || "*/*", output.mimeType) })).filter((value) => value.q > 0).sort((a, b) => b.q - a.q || a.index - b.index)[0]?.output;
|
|
88
|
+
}
|
|
89
|
+
export function createWaveform(channels, sampleRate, samples = 1000, precision = 4) {
|
|
90
|
+
if (!channels.length || sampleRate <= 0)
|
|
91
|
+
throw TypeError("Waveform requires channels and a positive sample rate");
|
|
92
|
+
const frames = channels[0].length;
|
|
93
|
+
if (channels.some((channel) => channel.length !== frames))
|
|
94
|
+
throw TypeError("Waveform channels must have equal lengths");
|
|
95
|
+
const size = Math.max(1, Math.ceil(frames / Math.max(1, Math.min(frames || 1, Math.floor(samples)))));
|
|
96
|
+
let absolute = 0;
|
|
97
|
+
const raw = channels.map((channel) => {
|
|
98
|
+
const peaks = [];
|
|
99
|
+
for (let start = 0;start < frames; start += size) {
|
|
100
|
+
let min = 1, max = -1;
|
|
101
|
+
for (let index = start;index < Math.min(frames, start + size); index++) {
|
|
102
|
+
const sample = channel[index] ?? 0, value = Number.isFinite(sample) ? Math.max(-1, Math.min(1, sample)) : 0;
|
|
103
|
+
min = Math.min(min, value);
|
|
104
|
+
max = Math.max(max, value);
|
|
105
|
+
absolute = Math.max(absolute, Math.abs(value));
|
|
106
|
+
}
|
|
107
|
+
peaks.push([min, max]);
|
|
108
|
+
}
|
|
109
|
+
return peaks;
|
|
110
|
+
}), divisor = absolute || 1, round = (value) => Number((value / divisor).toFixed(Math.max(0, Math.min(6, precision))));
|
|
111
|
+
return { version: 1, channels: channels.length, sampleRate, duration: frames / sampleRate, samplesPerPeak: size, peaks: raw.map((channel) => channel.flatMap(([min, max]) => [round(min), round(max)])) };
|
|
112
|
+
}
|
|
113
|
+
function time(seconds) {
|
|
114
|
+
const ms = Math.round(seconds * 1000);
|
|
115
|
+
return `${String(Math.floor(ms / 3600000)).padStart(2, "0")}:${String(Math.floor(ms % 3600000 / 60000)).padStart(2, "0")}:${String(Math.floor(ms % 60000 / 1000)).padStart(2, "0")}.${String(ms % 1000).padStart(3, "0")}`;
|
|
116
|
+
}
|
|
117
|
+
export function normalizeTranscript(language, input) {
|
|
118
|
+
if (!language.trim())
|
|
119
|
+
throw TypeError("Transcript language is required");
|
|
120
|
+
let end = 0;
|
|
121
|
+
const segments = input.map((item, index) => {
|
|
122
|
+
if (item.startTime < end || item.startTime < 0 || item.endTime <= item.startTime || !item.text.trim())
|
|
123
|
+
throw TypeError(`Invalid transcript segment ${index}`);
|
|
124
|
+
if (item.confidence !== void 0 && (item.confidence < 0 || item.confidence > 1))
|
|
125
|
+
throw TypeError(`Invalid transcript confidence ${index}`);
|
|
126
|
+
end = item.endTime;
|
|
127
|
+
return { ...item, text: item.text.trim(), id: index + 1 };
|
|
128
|
+
}), lines = ["WEBVTT", ""];
|
|
129
|
+
for (const item of segments)
|
|
130
|
+
lines.push(String(item.id), `${time(item.startTime)} --> ${time(item.endTime)}`, item.speaker ? `<v ${item.speaker}>${item.text}` : item.text, "");
|
|
131
|
+
return { language: language.trim().toLowerCase(), segments, vtt: lines.join(`
|
|
132
|
+
`) };
|
|
133
|
+
}
|
|
134
|
+
export function audioResponseHeaders(bytes, etag, contentType) {
|
|
135
|
+
return { "Accept-Ranges": "bytes", "Content-Length": String(bytes), "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable", ETag: `"${etag}"`, Vary: "Accept" };
|
|
136
|
+
}
|
|
137
|
+
export function signAudioAsset(path, expires, secret) {
|
|
138
|
+
return createHmac("sha256", secret).update(`${path}
|
|
139
|
+
${expires}`).digest("base64url");
|
|
140
|
+
}
|
|
141
|
+
export function verifyAudioAsset(path, expires, signature, secret, now = Date.now()) {
|
|
142
|
+
if (!Number.isInteger(expires) || expires * 1000 <= now)
|
|
143
|
+
return !1;
|
|
144
|
+
const expected = Buffer.from(signAudioAsset(path, expires, secret)), actual = Buffer.from(signature);
|
|
145
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
146
|
+
}
|
|
147
|
+
export async function createProtectedAudioPlaylist(segments, protection) {
|
|
148
|
+
if (!segments.length)
|
|
149
|
+
throw TypeError("Audio playlist requires segments");
|
|
150
|
+
if (protection && /[\r\n"]/.test(protection.keyUri))
|
|
151
|
+
throw TypeError("Invalid audio key URI");
|
|
152
|
+
const files = {}, lines = ["#EXTM3U", "#EXT-X-VERSION:7", `#EXT-X-TARGETDURATION:${Math.ceil(Math.max(...segments.map((item) => item.duration)))}`, "#EXT-X-PLAYLIST-TYPE:VOD"];
|
|
153
|
+
for (const [index, segment] of segments.entries()) {
|
|
154
|
+
if (!Number.isFinite(segment.duration) || segment.duration <= 0 || /[\r\n"]/.test(segment.uri))
|
|
155
|
+
throw TypeError(`Invalid audio segment ${index}`);
|
|
156
|
+
let data = segment.data;
|
|
157
|
+
if (protection) {
|
|
158
|
+
if (protection.key.byteLength !== 16)
|
|
159
|
+
throw TypeError("Audio HLS AES-128 key must contain 16 bytes");
|
|
160
|
+
const iv = new Uint8Array(16);
|
|
161
|
+
new DataView(iv.buffer).setBigUint64(8, BigInt(index));
|
|
162
|
+
const key = Uint8Array.from(protection.key), input = Uint8Array.from(data), cryptoKey = await crypto.subtle.importKey("raw", key.buffer, { name: "AES-CBC" }, !1, ["encrypt"]);
|
|
163
|
+
data = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv.buffer }, cryptoKey, input.buffer));
|
|
164
|
+
const ivHex = [...iv].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
165
|
+
lines.push(`#EXT-X-KEY:METHOD=AES-128,URI="${protection.keyUri}",IV=0x${ivHex}`);
|
|
166
|
+
}
|
|
167
|
+
lines.push(`#EXTINF:${segment.duration.toFixed(6)},`, segment.uri);
|
|
168
|
+
files[segment.uri] = data;
|
|
169
|
+
}
|
|
170
|
+
lines.push("#EXT-X-ENDLIST", "");
|
|
171
|
+
return { playlist: lines.join(`
|
|
172
|
+
`), files, encrypted: !!protection };
|
|
173
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stacksjs/audio",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"version": "0.70.163",
|
|
6
|
+
"description": "Native audio delivery planning for Stacks.",
|
|
7
|
+
"author": "Chris Breuer",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"funding": "https://github.com/sponsors/chrisbbreuer",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
13
|
+
"directory": "./storage/framework/core/audio"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"development": "./src/index.ts",
|
|
19
|
+
"bun": "./dist/index.js",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"module": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"README.md",
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "bun build.ts",
|
|
31
|
+
"typecheck": "bun tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "bun run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ts-audio/core": "^0.1.1",
|
|
36
|
+
"ts-video-player": "^0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"better-dx": "^0.2.17"
|
|
40
|
+
}
|
|
41
|
+
}
|