@flighthq/audio 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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/README.md +2 -2
- package/dist/audioBackend.d.ts +9 -0
- package/dist/audioBackend.d.ts.map +1 -0
- package/dist/audioBackend.js +57 -0
- package/dist/audioBackend.js.map +1 -0
- package/dist/audioFormat.js +2 -3
- package/dist/audioFormat.js.map +1 -1
- package/dist/audioResourceFrom.d.ts +3 -3
- package/dist/audioResourceFrom.d.ts.map +1 -1
- package/dist/audioResourceFrom.js +11 -12
- package/dist/audioResourceFrom.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/package.json +3 -2
- package/src/audioBackend.test.ts +136 -0
- package/src/audioFormat.test.ts +10 -6
- package/src/audioResourceFrom.test.ts +76 -37
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/audio`. The `@flight
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/audio@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/audio@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/audio)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AudioBackend, BackendExplanation } from '@flighthq/types/contract';
|
|
2
|
+
export declare function createWebAudioBackend(): AudioBackend;
|
|
3
|
+
export declare function explainAudioBackend(): BackendExplanation;
|
|
4
|
+
export declare function getAudioBackend(): AudioBackend;
|
|
5
|
+
export declare function installAudioHostBackend(backend: AudioBackend): void;
|
|
6
|
+
export declare function observeAudioHostResult(operation: string, succeeded: boolean): void;
|
|
7
|
+
export declare function resetAudioBackendForTest(): void;
|
|
8
|
+
export declare function setAudioBackend(backend: AudioBackend | null): void;
|
|
9
|
+
//# sourceMappingURL=audioBackend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audioBackend.d.ts","sourceRoot":"","sources":["../src/audioBackend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEjF,wBAAgB,qBAAqB,IAAI,YAAY,CAMpD;AAED,wBAAgB,mBAAmB,IAAI,kBAAkB,CAaxD;AAED,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAMnE;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAKlF;AAED,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAElE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createWebAudioBackend() {
|
|
2
|
+
return {
|
|
3
|
+
canPlayType(mimeType) {
|
|
4
|
+
return new Audio().canPlayType(mimeType) !== '';
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function explainAudioBackend() {
|
|
9
|
+
if (_custom !== null) {
|
|
10
|
+
return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
|
|
11
|
+
}
|
|
12
|
+
if (_host !== null) {
|
|
13
|
+
return {
|
|
14
|
+
conflict: _hostConflict,
|
|
15
|
+
layer: 'host',
|
|
16
|
+
operation: _hostObservation !== null ? _hostObservation.operation : null,
|
|
17
|
+
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
21
|
+
}
|
|
22
|
+
export function getAudioBackend() {
|
|
23
|
+
return _custom ?? _host ?? _sentinel;
|
|
24
|
+
}
|
|
25
|
+
export function installAudioHostBackend(backend) {
|
|
26
|
+
if (_host !== null) {
|
|
27
|
+
if (_host !== backend)
|
|
28
|
+
_hostConflict = true;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
_host = backend;
|
|
32
|
+
}
|
|
33
|
+
export function observeAudioHostResult(operation, succeeded) {
|
|
34
|
+
_hostObservation = {
|
|
35
|
+
operation,
|
|
36
|
+
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function resetAudioBackendForTest() {
|
|
40
|
+
_custom = null;
|
|
41
|
+
_host = null;
|
|
42
|
+
_hostConflict = false;
|
|
43
|
+
_hostObservation = null;
|
|
44
|
+
}
|
|
45
|
+
export function setAudioBackend(backend) {
|
|
46
|
+
_custom = backend;
|
|
47
|
+
}
|
|
48
|
+
let _custom = null;
|
|
49
|
+
let _host = null;
|
|
50
|
+
let _hostConflict = false;
|
|
51
|
+
let _hostObservation = null;
|
|
52
|
+
const _sentinel = {
|
|
53
|
+
canPlayType(_mimeType) {
|
|
54
|
+
return false;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=audioBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audioBackend.js","sourceRoot":"","sources":["../src/audioBackend.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,WAAW,CAAC,QAAgB;YAC1B,OAAO,IAAI,KAAK,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IAChG,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,QAAQ,EAAE,aAAa;YACvB,KAAK,EAAE,MAAM;YACb,SAAS,EAAE,gBAAgB,KAAK,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;YACxE,SAAS,EAAE,gBAAgB,KAAK,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;SACjF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,OAAO,IAAI,KAAK,IAAI,SAAS,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAqB;IAC3D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,IAAI,KAAK,KAAK,OAAO;YAAE,aAAa,GAAG,IAAI,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,KAAK,GAAG,OAAO,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,SAAiB,EAAE,SAAkB;IAC1E,gBAAgB,GAAG;QACjB,SAAS;QACT,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,yBAAyB;KAC/D,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB;IACtC,OAAO,GAAG,IAAI,CAAC;IACf,KAAK,GAAG,IAAI,CAAC;IACb,aAAa,GAAG,KAAK,CAAC;IACtB,gBAAgB,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAA4B;IAC1D,OAAO,GAAG,OAAO,CAAC;AACpB,CAAC;AAED,IAAI,OAAO,GAAwB,IAAI,CAAC;AACxC,IAAI,KAAK,GAAwB,IAAI,CAAC;AACtC,IAAI,aAAa,GAAG,KAAK,CAAC;AAC1B,IAAI,gBAAgB,GAAqF,IAAI,CAAC;AAE9G,MAAM,SAAS,GAAiB;IAC9B,WAAW,CAAC,SAAiB;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAC"}
|
package/dist/audioFormat.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
// canPlayType. Returns false for the empty string and for any type the browser reports it cannot play.
|
|
1
|
+
import { getAudioBackend } from './audioBackend';
|
|
3
2
|
export function canPlayAudioType(mimeType) {
|
|
4
3
|
if (mimeType === '')
|
|
5
4
|
return false;
|
|
6
|
-
return
|
|
5
|
+
return getAudioBackend().canPlayType(mimeType);
|
|
7
6
|
}
|
|
8
7
|
// Sniffs the container MIME type from a buffer's leading magic bytes, or null when unrecognized.
|
|
9
8
|
// Mirrors detectImageMimeType. Covers wav, flac, ogg, mp3 (ID3 or MPEG frame sync), m4a/aac
|
package/dist/audioFormat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioFormat.js","sourceRoot":"","sources":["../src/audioFormat.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"audioFormat.js","sourceRoot":"","sources":["../src/audioFormat.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { AudioResource, AudioResourceUrl } from '@flighthq/types/contract';
|
|
1
|
+
import type { AudioResource, AudioResourceUrl, HasNetHttp } from '@flighthq/types/contract';
|
|
2
2
|
export declare function createAudioResourceFromSamples(channels: readonly Float32Array[], sampleRate: number): AudioResource;
|
|
3
3
|
export declare function loadAudioResourceFromBase64(context: AudioContext, base64: string, mimeType: string, signal?: AbortSignal): Promise<AudioResource>;
|
|
4
4
|
export declare function loadAudioResourceFromBlob(context: AudioContext, blob: Blob, signal?: AbortSignal): Promise<AudioResource>;
|
|
5
5
|
export declare function loadAudioResourceFromBytes(context: AudioContext, bytes: Uint8Array, mimeType?: string, signal?: AbortSignal): Promise<AudioResource>;
|
|
6
|
-
export declare function loadAudioResourceFromUrl(context: AudioContext, url: string, signal?: AbortSignal): Promise<AudioResource>;
|
|
7
|
-
export declare function loadAudioResourceFromUrls(context: AudioContext, sources: readonly AudioResourceUrl[], signal?: AbortSignal): Promise<AudioResource>;
|
|
6
|
+
export declare function loadAudioResourceFromUrl(host: HasNetHttp, context: AudioContext, url: string, signal?: AbortSignal): Promise<AudioResource>;
|
|
7
|
+
export declare function loadAudioResourceFromUrls(host: HasNetHttp, context: AudioContext, sources: readonly AudioResourceUrl[], signal?: AbortSignal): Promise<AudioResource>;
|
|
8
8
|
export declare function selectAudioResourceUrl(sources: readonly AudioResourceUrl[]): string | null;
|
|
9
9
|
//# sourceMappingURL=audioResourceFrom.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioResourceFrom.d.ts","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audioResourceFrom.d.ts","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAS5F,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,CAUnH;AAID,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAKxB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,EACV,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAGxB;AAKD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAWxB;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAIxB;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,GAAG,IAAI,CAM1F"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sendNetRequest } from '@flighthq/net/contract';
|
|
1
2
|
import { canPlayAudioType, inferAudioMimeType } from './audioFormat';
|
|
2
3
|
import { createAudioResource } from './audioResource';
|
|
3
4
|
// Builds a resource from raw PCM channel data without needing an AudioContext. Each entry in
|
|
@@ -44,24 +45,22 @@ export async function loadAudioResourceFromBytes(context, bytes, mimeType, signa
|
|
|
44
45
|
signal?.throwIfAborted();
|
|
45
46
|
return createAudioResource(audioBuffer);
|
|
46
47
|
}
|
|
47
|
-
export async function loadAudioResourceFromUrl(context, url, signal) {
|
|
48
|
-
const response = await
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// a chance. Reporting the status keeps this function's existing reject-on-failure contract; whether
|
|
53
|
-
// this family should reject at all or return the empty-resource sentinel is the separate parked
|
|
54
|
-
// question in the assessment Backlog.
|
|
48
|
+
export async function loadAudioResourceFromUrl(host, context, url, signal) {
|
|
49
|
+
const response = await sendNetRequest(host, { method: 'GET', responseType: 'arraybuffer', url }, signal === undefined ? undefined : { signal });
|
|
50
|
+
// The NetBackend reports network failures and non-2xx responses alike through the response, before
|
|
51
|
+
// the audio decoder can misdiagnose an HTTP error body as invalid audio. This function retains its
|
|
52
|
+
// existing reject-on-failure contract; the transport itself remains caller-replaceable.
|
|
55
53
|
if (!response.ok)
|
|
56
54
|
throw new Error(`Failed to load audio: ${url} (${response.status} ${response.statusText})`);
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
if (!(response.body instanceof ArrayBuffer))
|
|
56
|
+
throw new Error(`Failed to load audio: ${url} (invalid body)`);
|
|
57
|
+
return loadAudioResourceFromBytes(context, new Uint8Array(response.body), response.headers['content-type'], signal);
|
|
59
58
|
}
|
|
60
|
-
export async function loadAudioResourceFromUrls(context, sources, signal) {
|
|
59
|
+
export async function loadAudioResourceFromUrls(host, context, sources, signal) {
|
|
61
60
|
const selected = selectAudioResourceUrl(sources);
|
|
62
61
|
if (selected === null)
|
|
63
62
|
return createAudioResource();
|
|
64
|
-
return loadAudioResourceFromUrl(context, selected, signal);
|
|
63
|
+
return loadAudioResourceFromUrl(host, context, selected, signal);
|
|
65
64
|
}
|
|
66
65
|
// Picks the first source URL the environment can play, or null when none is playable. A source's
|
|
67
66
|
// explicit `type` wins; otherwise the MIME type is inferred from the URL extension.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audioResourceFrom.js","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"audioResourceFrom.js","sourceRoot":"","sources":["../src/audioResourceFrom.ts"],"names":[],"mappings":"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"}
|
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/audio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/net": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { AudioBackend } from '@flighthq/types/contract';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
explainAudioBackend,
|
|
6
|
+
getAudioBackend,
|
|
7
|
+
installAudioHostBackend,
|
|
8
|
+
observeAudioHostResult,
|
|
9
|
+
resetAudioBackendForTest,
|
|
10
|
+
setAudioBackend,
|
|
11
|
+
} from './audioBackend';
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
resetAudioBackendForTest();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createWebAudioBackend', () => {
|
|
18
|
+
it('is tested via host-web', () => {
|
|
19
|
+
expect(true).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('explainAudioBackend', () => {
|
|
24
|
+
it('reports host-not-enabled when no backend is installed', () => {
|
|
25
|
+
const explanation = explainAudioBackend();
|
|
26
|
+
expect(explanation.layer).toBe('host-not-enabled');
|
|
27
|
+
expect(explanation.conflict).toBe(false);
|
|
28
|
+
expect(explanation.viability).toBe('unobserved');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('reports host layer when host backend is installed', () => {
|
|
32
|
+
installAudioHostBackend({ canPlayType: () => false });
|
|
33
|
+
const explanation = explainAudioBackend();
|
|
34
|
+
expect(explanation.layer).toBe('host');
|
|
35
|
+
expect(explanation.viability).toBe('unobserved');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('reports custom layer when custom backend is set', () => {
|
|
39
|
+
installAudioHostBackend({ canPlayType: () => false });
|
|
40
|
+
setAudioBackend({ canPlayType: () => true });
|
|
41
|
+
const explanation = explainAudioBackend();
|
|
42
|
+
expect(explanation.layer).toBe('custom');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reports observed viability after observeAudioHostResult', () => {
|
|
46
|
+
installAudioHostBackend({ canPlayType: () => true });
|
|
47
|
+
observeAudioHostResult('canPlayType', true);
|
|
48
|
+
const explanation = explainAudioBackend();
|
|
49
|
+
expect(explanation.viability).toBe('available');
|
|
50
|
+
expect(explanation.operation).toBe('canPlayType');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reports runtime-api-unavailable on failed observation', () => {
|
|
54
|
+
installAudioHostBackend({ canPlayType: () => false });
|
|
55
|
+
observeAudioHostResult('canPlayType', false);
|
|
56
|
+
const explanation = explainAudioBackend();
|
|
57
|
+
expect(explanation.viability).toBe('runtime-api-unavailable');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('getAudioBackend', () => {
|
|
62
|
+
it('returns sentinel when no backend is installed', () => {
|
|
63
|
+
const backend = getAudioBackend();
|
|
64
|
+
expect(backend.canPlayType('audio/mpeg')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns host backend when installed', () => {
|
|
68
|
+
const host: AudioBackend = { canPlayType: () => true };
|
|
69
|
+
installAudioHostBackend(host);
|
|
70
|
+
expect(getAudioBackend()).toBe(host);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns custom backend over host', () => {
|
|
74
|
+
const host: AudioBackend = { canPlayType: () => false };
|
|
75
|
+
const custom: AudioBackend = { canPlayType: () => true };
|
|
76
|
+
installAudioHostBackend(host);
|
|
77
|
+
setAudioBackend(custom);
|
|
78
|
+
expect(getAudioBackend()).toBe(custom);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('installAudioHostBackend', () => {
|
|
83
|
+
it('installs the host backend', () => {
|
|
84
|
+
const host: AudioBackend = { canPlayType: () => true };
|
|
85
|
+
installAudioHostBackend(host);
|
|
86
|
+
expect(getAudioBackend()).toBe(host);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detects conflict on double install with different backend', () => {
|
|
90
|
+
installAudioHostBackend({ canPlayType: () => false });
|
|
91
|
+
installAudioHostBackend({ canPlayType: () => true });
|
|
92
|
+
expect(explainAudioBackend().conflict).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not flag conflict on same backend', () => {
|
|
96
|
+
const host: AudioBackend = { canPlayType: () => false };
|
|
97
|
+
installAudioHostBackend(host);
|
|
98
|
+
installAudioHostBackend(host);
|
|
99
|
+
expect(explainAudioBackend().conflict).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('observeAudioHostResult', () => {
|
|
104
|
+
it('records the operation and viability', () => {
|
|
105
|
+
installAudioHostBackend({ canPlayType: () => true });
|
|
106
|
+
observeAudioHostResult('canPlayType', true);
|
|
107
|
+
const explanation = explainAudioBackend();
|
|
108
|
+
expect(explanation.operation).toBe('canPlayType');
|
|
109
|
+
expect(explanation.viability).toBe('available');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('resetAudioBackendForTest', () => {
|
|
114
|
+
it('clears all state', () => {
|
|
115
|
+
installAudioHostBackend({ canPlayType: () => true });
|
|
116
|
+
setAudioBackend({ canPlayType: () => true });
|
|
117
|
+
observeAudioHostResult('canPlayType', true);
|
|
118
|
+
resetAudioBackendForTest();
|
|
119
|
+
expect(explainAudioBackend().layer).toBe('host-not-enabled');
|
|
120
|
+
expect(getAudioBackend().canPlayType('audio/mpeg')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('setAudioBackend', () => {
|
|
125
|
+
it('overrides the active backend', () => {
|
|
126
|
+
const custom: AudioBackend = { canPlayType: () => true };
|
|
127
|
+
setAudioBackend(custom);
|
|
128
|
+
expect(getAudioBackend()).toBe(custom);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('clears the custom backend when null', () => {
|
|
132
|
+
setAudioBackend({ canPlayType: () => true });
|
|
133
|
+
setAudioBackend(null);
|
|
134
|
+
expect(getAudioBackend().canPlayType('audio/mpeg')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
package/src/audioFormat.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resetAudioBackendForTest, setAudioBackend } from './audioBackend';
|
|
1
2
|
import {
|
|
2
3
|
canPlayAudioType,
|
|
3
4
|
detectAudioMimeType,
|
|
@@ -8,26 +9,29 @@ import {
|
|
|
8
9
|
|
|
9
10
|
describe('canPlayAudioType', () => {
|
|
10
11
|
beforeEach(() => {
|
|
11
|
-
|
|
12
|
-
type === 'audio/mpeg' ? 'probably' : '',
|
|
13
|
-
);
|
|
12
|
+
setAudioBackend({ canPlayType: (type: string) => type === 'audio/mpeg' });
|
|
14
13
|
});
|
|
15
14
|
|
|
16
15
|
afterEach(() => {
|
|
17
|
-
|
|
16
|
+
resetAudioBackendForTest();
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
it('returns false for the empty string without probing', () => {
|
|
21
20
|
expect(canPlayAudioType('')).toBe(false);
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
it('returns true for a type the
|
|
23
|
+
it('returns true for a type the backend reports as playable', () => {
|
|
25
24
|
expect(canPlayAudioType('audio/mpeg')).toBe(true);
|
|
26
25
|
});
|
|
27
26
|
|
|
28
|
-
it('returns false for a type the
|
|
27
|
+
it('returns false for a type the backend cannot play', () => {
|
|
29
28
|
expect(canPlayAudioType('audio/x-unknown')).toBe(false);
|
|
30
29
|
});
|
|
30
|
+
|
|
31
|
+
it('returns false when no backend is installed (sentinel)', () => {
|
|
32
|
+
resetAudioBackendForTest();
|
|
33
|
+
expect(canPlayAudioType('audio/mpeg')).toBe(false);
|
|
34
|
+
});
|
|
31
35
|
});
|
|
32
36
|
|
|
33
37
|
describe('detectAudioMimeType', () => {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { HasNetHttp, NetBackend } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { resetAudioBackendForTest, setAudioBackend } from './audioBackend';
|
|
1
4
|
import {
|
|
2
5
|
createAudioResourceFromSamples,
|
|
3
6
|
loadAudioResourceFromBase64,
|
|
@@ -8,6 +11,16 @@ import {
|
|
|
8
11
|
selectAudioResourceUrl,
|
|
9
12
|
} from './audioResourceFrom';
|
|
10
13
|
|
|
14
|
+
function fakeNetHost(backend?: NetBackend): HasNetHttp {
|
|
15
|
+
return {
|
|
16
|
+
net: {
|
|
17
|
+
http: backend ?? {
|
|
18
|
+
sendNetRequest: async () => ({ status: 200, statusText: 'OK', ok: true, headers: {}, body: null, url: '' }),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
const decodedBuffer = { duration: 1 } as AudioBuffer;
|
|
12
25
|
|
|
13
26
|
const mockContext = {
|
|
@@ -55,6 +68,7 @@ beforeEach(() => {
|
|
|
55
68
|
});
|
|
56
69
|
|
|
57
70
|
afterEach(() => {
|
|
71
|
+
resetAudioBackendForTest();
|
|
58
72
|
vi.unstubAllGlobals();
|
|
59
73
|
vi.restoreAllMocks();
|
|
60
74
|
(mockContext.decodeAudioData as ReturnType<typeof vi.fn>).mockClear();
|
|
@@ -174,51 +188,73 @@ describe('loadAudioResourceFromBytes', () => {
|
|
|
174
188
|
});
|
|
175
189
|
|
|
176
190
|
describe('loadAudioResourceFromUrl', () => {
|
|
177
|
-
it('
|
|
178
|
-
vi.
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
it('routes URL loading through the host NetBackend', async () => {
|
|
192
|
+
const mockSendNetRequest = vi.fn().mockResolvedValue({
|
|
193
|
+
body: new ArrayBuffer(8),
|
|
194
|
+
headers: { 'content-type': 'audio/mpeg' },
|
|
195
|
+
ok: true,
|
|
196
|
+
status: 200,
|
|
197
|
+
statusText: 'OK',
|
|
198
|
+
url: 'sound.mp3',
|
|
199
|
+
});
|
|
200
|
+
const host = fakeNetHost({ sendNetRequest: mockSendNetRequest });
|
|
201
|
+
|
|
202
|
+
const resource = await loadAudioResourceFromUrl(host, mockContext, 'sound.mp3');
|
|
203
|
+
|
|
204
|
+
expect(resource.buffer).toBe(decodedBuffer);
|
|
205
|
+
expect(mockSendNetRequest).toHaveBeenCalledWith(
|
|
206
|
+
{ method: 'GET', responseType: 'arraybuffer', url: 'sound.mp3' },
|
|
207
|
+
undefined,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('decodes a successful arraybuffer response into a resource', async () => {
|
|
212
|
+
const host = fakeNetHost({
|
|
213
|
+
sendNetRequest: async () => ({
|
|
214
|
+
body: new ArrayBuffer(8),
|
|
215
|
+
headers: { 'content-type': 'audio/mpeg' },
|
|
181
216
|
ok: true,
|
|
182
|
-
|
|
183
|
-
|
|
217
|
+
status: 200,
|
|
218
|
+
statusText: 'OK',
|
|
219
|
+
url: 'sound.mp3',
|
|
184
220
|
}),
|
|
185
|
-
);
|
|
186
|
-
const resource = await loadAudioResourceFromUrl(mockContext, 'sound.mp3');
|
|
221
|
+
});
|
|
222
|
+
const resource = await loadAudioResourceFromUrl(host, mockContext, 'sound.mp3');
|
|
187
223
|
expect(resource.buffer).toBe(decodedBuffer);
|
|
188
224
|
});
|
|
189
225
|
|
|
190
|
-
// fetch resolves for 404/500, so without a status check the error page reaches the decoder and the
|
|
191
|
-
// caller is told the codec failed. The assertion pins the status, not merely that it rejected.
|
|
192
226
|
it('rejects with the HTTP status rather than decoding an error response', async () => {
|
|
193
227
|
const decodeAudioData = vi.fn().mockResolvedValue(decodedBuffer);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
const host = fakeNetHost({
|
|
229
|
+
sendNetRequest: async () => ({
|
|
230
|
+
body: null,
|
|
231
|
+
headers: {},
|
|
197
232
|
ok: false,
|
|
198
233
|
status: 404,
|
|
199
234
|
statusText: 'Not Found',
|
|
200
|
-
|
|
201
|
-
headers: new Headers({ 'content-type': 'text/html' }),
|
|
235
|
+
url: 'missing.mp3',
|
|
202
236
|
}),
|
|
203
|
-
);
|
|
237
|
+
});
|
|
204
238
|
await expect(
|
|
205
|
-
loadAudioResourceFromUrl({ decodeAudioData } as unknown as AudioContext, 'missing.mp3'),
|
|
239
|
+
loadAudioResourceFromUrl(host, { decodeAudioData } as unknown as AudioContext, 'missing.mp3'),
|
|
206
240
|
).rejects.toThrow('Failed to load audio: missing.mp3 (404 Not Found)');
|
|
207
241
|
expect(decodeAudioData).not.toHaveBeenCalled();
|
|
208
242
|
});
|
|
209
243
|
|
|
210
244
|
it('rejects when the signal aborts while the decode is in flight', async () => {
|
|
211
245
|
const { context, finishDecode } = createPendingDecodeContext();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
246
|
+
const host = fakeNetHost({
|
|
247
|
+
sendNetRequest: async () => ({
|
|
248
|
+
body: new ArrayBuffer(8),
|
|
249
|
+
headers: { 'content-type': 'audio/mpeg' },
|
|
215
250
|
ok: true,
|
|
216
|
-
|
|
217
|
-
|
|
251
|
+
status: 200,
|
|
252
|
+
statusText: 'OK',
|
|
253
|
+
url: 'sound.mp3',
|
|
218
254
|
}),
|
|
219
|
-
);
|
|
255
|
+
});
|
|
220
256
|
const controller = new AbortController();
|
|
221
|
-
const promise = loadAudioResourceFromUrl(context, 'sound.mp3', controller.signal);
|
|
257
|
+
const promise = loadAudioResourceFromUrl(host, context, 'sound.mp3', controller.signal);
|
|
222
258
|
await Promise.resolve();
|
|
223
259
|
controller.abort(new Error('cancelled'));
|
|
224
260
|
finishDecode();
|
|
@@ -228,33 +264,36 @@ describe('loadAudioResourceFromUrl', () => {
|
|
|
228
264
|
|
|
229
265
|
describe('loadAudioResourceFromUrls', () => {
|
|
230
266
|
it('resolves with a null-buffer resource when sources is empty', async () => {
|
|
231
|
-
const
|
|
267
|
+
const host = fakeNetHost();
|
|
268
|
+
const resource = await loadAudioResourceFromUrls(host, mockContext, []);
|
|
232
269
|
expect(resource.buffer).toBeNull();
|
|
233
270
|
});
|
|
234
271
|
|
|
235
272
|
it('loads the first playable source', async () => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
273
|
+
setAudioBackend({ canPlayType: (type: string) => type === 'audio/ogg' });
|
|
274
|
+
const mockSendNetRequest = vi.fn().mockResolvedValue({
|
|
275
|
+
body: new ArrayBuffer(8),
|
|
276
|
+
headers: { 'content-type': 'audio/ogg' },
|
|
240
277
|
ok: true,
|
|
241
|
-
|
|
242
|
-
|
|
278
|
+
status: 200,
|
|
279
|
+
statusText: 'OK',
|
|
280
|
+
url: 'sound.ogg',
|
|
243
281
|
});
|
|
244
|
-
|
|
282
|
+
const host = fakeNetHost({ sendNetRequest: mockSendNetRequest });
|
|
245
283
|
|
|
246
|
-
const resource = await loadAudioResourceFromUrls(mockContext, [{ url: 'sound.mp3' }, { url: 'sound.ogg' }]);
|
|
284
|
+
const resource = await loadAudioResourceFromUrls(host, mockContext, [{ url: 'sound.mp3' }, { url: 'sound.ogg' }]);
|
|
247
285
|
|
|
248
286
|
expect(resource.buffer).toBe(decodedBuffer);
|
|
249
|
-
expect(
|
|
287
|
+
expect(mockSendNetRequest).toHaveBeenCalledWith(
|
|
288
|
+
{ method: 'GET', responseType: 'arraybuffer', url: 'sound.ogg' },
|
|
289
|
+
undefined,
|
|
290
|
+
);
|
|
250
291
|
});
|
|
251
292
|
});
|
|
252
293
|
|
|
253
294
|
describe('selectAudioResourceUrl', () => {
|
|
254
295
|
beforeEach(() => {
|
|
255
|
-
|
|
256
|
-
type === 'audio/ogg' ? 'maybe' : '',
|
|
257
|
-
);
|
|
296
|
+
setAudioBackend({ canPlayType: (type: string) => type === 'audio/ogg' });
|
|
258
297
|
});
|
|
259
298
|
|
|
260
299
|
it('returns the first source whose inferred type is playable', () => {
|