@aippy/runtime 0.1.0 → 0.2.0-dev.1
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 +22 -0
- package/dist/audio/index.d.ts +13 -0
- package/dist/audio/index.js +9 -0
- package/dist/audio/patchAudioContext.d.ts +30 -0
- package/dist/audio/types.d.ts +52 -0
- package/dist/audio/useAudioContext.d.ts +66 -0
- package/dist/audio/utils.d.ts +24 -0
- package/dist/audio.d.ts +2 -0
- package/dist/core/index.js +1 -1
- package/dist/index/index.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/tweaks/types.d.ts +52 -0
- package/dist/useAudioContext-DSbHyklm.js +186 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -66,11 +66,33 @@ await pwa.registerServiceWorker('/sw.js');
|
|
|
66
66
|
await pwa.sendNotification('Hello!');
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### Audio (iOS Silent Mode Compatible)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { patchAudioContext } from '@aippy/runtime/audio';
|
|
73
|
+
|
|
74
|
+
// Create and patch AudioContext to bypass iOS silent mode
|
|
75
|
+
const ctx = new AudioContext();
|
|
76
|
+
const patchedCtx = patchAudioContext(ctx);
|
|
77
|
+
|
|
78
|
+
// Unlock audio on user interaction (required on iOS)
|
|
79
|
+
button.onclick = async () => {
|
|
80
|
+
await patchedCtx.unlock();
|
|
81
|
+
|
|
82
|
+
// Use native Web Audio API as normal
|
|
83
|
+
const osc = patchedCtx.createOscillator();
|
|
84
|
+
osc.connect(patchedCtx.destination);
|
|
85
|
+
osc.start();
|
|
86
|
+
osc.stop(patchedCtx.currentTime + 1);
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
69
90
|
## Packages
|
|
70
91
|
|
|
71
92
|
- `@aippy/runtime/core` - Core types and configuration
|
|
72
93
|
- `@aippy/runtime/device` - Device APIs (camera, geolocation, sensors, file system)
|
|
73
94
|
- `@aippy/runtime/utils` - Platform detection, performance monitoring, PWA utilities
|
|
95
|
+
- `@aippy/runtime/audio` - iOS-compatible Web Audio API wrapper
|
|
74
96
|
|
|
75
97
|
## Publishing
|
|
76
98
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio module for iOS-compatible Web Audio API
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to bypass iOS silent mode restrictions
|
|
5
|
+
* on Web Audio API playback.
|
|
6
|
+
*
|
|
7
|
+
* @module @aippy/runtime/audio
|
|
8
|
+
*/
|
|
9
|
+
export { patchAudioContext } from './patchAudioContext';
|
|
10
|
+
export type { AudioContextPatchOptions, MediaElementType, PatchedAudioContext, } from './types';
|
|
11
|
+
export { createHiddenMediaElement, createHiddenVideoElement, isIOSDevice, isMediaStreamAudioSupported, } from './utils';
|
|
12
|
+
export { useAudioContext } from './useAudioContext';
|
|
13
|
+
export type { UseAudioContextOptions, UseAudioContextReturn } from './useAudioContext';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AudioContextPatchOptions, PatchedAudioContext } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Patches an AudioContext to bypass iOS silent mode restrictions
|
|
4
|
+
*
|
|
5
|
+
* On iOS devices, Web Audio API is affected by the hardware silent switch.
|
|
6
|
+
* This function routes audio through a MediaStreamAudioDestinationNode
|
|
7
|
+
* connected to a hidden video element, which bypasses the restriction.
|
|
8
|
+
*
|
|
9
|
+
* On non-iOS devices, this function returns the original context with
|
|
10
|
+
* minimal modifications (zero overhead).
|
|
11
|
+
*
|
|
12
|
+
* @param audioContext - The AudioContext to patch
|
|
13
|
+
* @param options - Configuration options
|
|
14
|
+
* @returns Enhanced AudioContext with iOS compatibility
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const ctx = new AudioContext();
|
|
19
|
+
* const patchedCtx = patchAudioContext(ctx);
|
|
20
|
+
*
|
|
21
|
+
* // Unlock on user interaction (iOS only)
|
|
22
|
+
* button.onclick = () => patchedCtx.unlock();
|
|
23
|
+
*
|
|
24
|
+
* // Use native Web Audio API normally
|
|
25
|
+
* const osc = patchedCtx.createOscillator();
|
|
26
|
+
* osc.connect(patchedCtx.destination); // Auto-routed on iOS
|
|
27
|
+
* osc.start();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function patchAudioContext(audioContext: AudioContext, options?: AudioContextPatchOptions): PatchedAudioContext;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media element type for MediaStream playback
|
|
3
|
+
*/
|
|
4
|
+
export type MediaElementType = 'video' | 'audio';
|
|
5
|
+
/**
|
|
6
|
+
* Options for patching AudioContext
|
|
7
|
+
*/
|
|
8
|
+
export interface AudioContextPatchOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Force enable patching even on non-iOS devices
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
forceEnable?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Automatically cleanup media element when AudioContext is closed
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
autoCleanup?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Show media element for debugging purposes
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Media element type to use for MediaStream playback
|
|
26
|
+
* Note: 'video' is recommended for better iOS compatibility
|
|
27
|
+
* @default 'video'
|
|
28
|
+
*/
|
|
29
|
+
mediaElementType?: MediaElementType;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extended AudioContext with iOS compatibility features
|
|
33
|
+
*/
|
|
34
|
+
export interface PatchedAudioContext extends AudioContext {
|
|
35
|
+
/**
|
|
36
|
+
* Unlock audio playback on iOS (must be called in user interaction)
|
|
37
|
+
* @returns Promise that resolves when unlocked
|
|
38
|
+
*/
|
|
39
|
+
unlock(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Manually cleanup patched resources
|
|
42
|
+
*/
|
|
43
|
+
cleanup(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Indicates whether the context has been patched
|
|
46
|
+
*/
|
|
47
|
+
readonly isPatched: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Reference to the original AudioDestinationNode
|
|
50
|
+
*/
|
|
51
|
+
readonly originalDestination: AudioDestinationNode;
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PatchedAudioContext, AudioContextPatchOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Options for useAudioContext hook
|
|
4
|
+
*/
|
|
5
|
+
export interface UseAudioContextOptions extends AudioContextPatchOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Auto-unlock audio on first user interaction
|
|
8
|
+
* @default true
|
|
9
|
+
*/
|
|
10
|
+
autoUnlock?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Return type for useAudioContext hook
|
|
14
|
+
*/
|
|
15
|
+
export interface UseAudioContextReturn {
|
|
16
|
+
/**
|
|
17
|
+
* Patched AudioContext instance
|
|
18
|
+
*/
|
|
19
|
+
audioContext: PatchedAudioContext | null;
|
|
20
|
+
/**
|
|
21
|
+
* Whether audio has been unlocked (ready for playback)
|
|
22
|
+
*/
|
|
23
|
+
isUnlocked: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Manually unlock audio (call during user interaction)
|
|
26
|
+
*/
|
|
27
|
+
unlock: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* React hook for managing AudioContext lifecycle with iOS silent mode bypass
|
|
31
|
+
*
|
|
32
|
+
* Automatically handles:
|
|
33
|
+
* - AudioContext creation and patching
|
|
34
|
+
* - Cleanup on unmount
|
|
35
|
+
* - Optional auto-unlock on first user interaction
|
|
36
|
+
*
|
|
37
|
+
* Note: Built for React 19 - no useCallback needed for optimal performance
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options
|
|
40
|
+
* @returns AudioContext instance, unlock status, and unlock function
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function AudioComponent() {
|
|
45
|
+
* const { audioContext, isUnlocked, unlock } = useAudioContext();
|
|
46
|
+
*
|
|
47
|
+
* const playSound = async () => {
|
|
48
|
+
* if (!audioContext) return;
|
|
49
|
+
*
|
|
50
|
+
* // Unlock if needed (handles iOS restrictions)
|
|
51
|
+
* if (!isUnlocked) {
|
|
52
|
+
* await unlock();
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* // Play sound using Web Audio API
|
|
56
|
+
* const osc = audioContext.createOscillator();
|
|
57
|
+
* osc.connect(audioContext.destination);
|
|
58
|
+
* osc.start();
|
|
59
|
+
* osc.stop(audioContext.currentTime + 0.3);
|
|
60
|
+
* };
|
|
61
|
+
*
|
|
62
|
+
* return <button onClick={playSound}>Play Sound</button>;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function useAudioContext(options?: UseAudioContextOptions): UseAudioContextReturn;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if the current device is running iOS/iPadOS
|
|
3
|
+
* @returns true if device is iOS/iPadOS
|
|
4
|
+
*/
|
|
5
|
+
export declare function isIOSDevice(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Checks if MediaStreamAudioDestinationNode is supported by the browser
|
|
8
|
+
* @returns true if supported
|
|
9
|
+
*/
|
|
10
|
+
export declare function isMediaStreamAudioSupported(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a hidden media element for MediaStream playback
|
|
13
|
+
* @param type - Element type ('video' or 'audio')
|
|
14
|
+
* @param debug - If true, makes the element visible for debugging
|
|
15
|
+
* @returns HTMLMediaElement (HTMLVideoElement or HTMLAudioElement)
|
|
16
|
+
*/
|
|
17
|
+
export declare function createHiddenMediaElement(type?: 'video' | 'audio', debug?: boolean): HTMLVideoElement | HTMLAudioElement;
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Use createHiddenMediaElement instead
|
|
20
|
+
* Creates a hidden video element for MediaStream playback
|
|
21
|
+
* @param debug - If true, makes the video element visible for debugging
|
|
22
|
+
* @returns HTMLVideoElement
|
|
23
|
+
*/
|
|
24
|
+
export declare function createHiddenVideoElement(debug?: boolean): HTMLVideoElement;
|
package/dist/audio.d.ts
ADDED
package/dist/core/index.js
CHANGED
package/dist/index/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { A, E, c } from "../errors-DAz5_jDJ.js";
|
|
|
3
3
|
import { CameraAPI, FileSystemAPI, GeolocationAPI, SensorsAPI, camera, fileSystem, geolocation, sensors, vibrate } from "../device/index.js";
|
|
4
4
|
import { c as c2, a, P, b, p, d } from "../pwa-BkviTQoN.js";
|
|
5
5
|
import { a as a2, b as b2 } from "../useTweaks-mK5PAWOs.js";
|
|
6
|
+
import { c as c3, a as a3, i, b as b3, p as p2, u } from "../useAudioContext-DSbHyklm.js";
|
|
6
7
|
export {
|
|
7
8
|
A as AippyRuntimeError,
|
|
8
9
|
CameraAPI,
|
|
@@ -20,14 +21,20 @@ export {
|
|
|
20
21
|
b2 as aippyTweaksRuntime,
|
|
21
22
|
camera,
|
|
22
23
|
c as createError,
|
|
24
|
+
c3 as createHiddenMediaElement,
|
|
25
|
+
a3 as createHiddenVideoElement,
|
|
23
26
|
fileSystem,
|
|
24
27
|
geolocation,
|
|
25
28
|
getConfigFromEnv,
|
|
26
29
|
getVersionInfo,
|
|
30
|
+
i as isIOSDevice,
|
|
31
|
+
b3 as isMediaStreamAudioSupported,
|
|
27
32
|
mergeConfig,
|
|
33
|
+
p2 as patchAudioContext,
|
|
28
34
|
b as performanceMonitor,
|
|
29
35
|
p as platform,
|
|
30
36
|
d as pwa,
|
|
31
37
|
sensors,
|
|
38
|
+
u as useAudioContext,
|
|
32
39
|
vibrate
|
|
33
40
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Window {
|
|
3
|
+
aippyTweaksRuntime?: {
|
|
4
|
+
tweaks: (config: any) => any;
|
|
5
|
+
tweaksInstance?: Record<string, any>;
|
|
6
|
+
};
|
|
7
|
+
processNativeData?: (data: any) => void;
|
|
8
|
+
}
|
|
9
|
+
interface WebKit {
|
|
10
|
+
messageHandlers: {
|
|
11
|
+
aippyListener: {
|
|
12
|
+
postMessage: (data: any) => void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export interface TweakItem {
|
|
18
|
+
id: string;
|
|
19
|
+
index: number;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
group?: string;
|
|
23
|
+
type: 'number' | 'color' | 'boolean' | 'text' | 'image';
|
|
24
|
+
value: number | string | boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface NumberTweakItem extends TweakItem {
|
|
27
|
+
type: 'number';
|
|
28
|
+
min?: number;
|
|
29
|
+
max?: number;
|
|
30
|
+
step?: number;
|
|
31
|
+
value: number;
|
|
32
|
+
}
|
|
33
|
+
export interface BooleanTweakItem extends TweakItem {
|
|
34
|
+
type: 'boolean';
|
|
35
|
+
value: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface TextTweakItem extends TweakItem {
|
|
38
|
+
type: 'text';
|
|
39
|
+
value: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ColorTweakItem extends TweakItem {
|
|
42
|
+
type: 'color';
|
|
43
|
+
value: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ImageTweakItem extends TweakItem {
|
|
46
|
+
type: 'image';
|
|
47
|
+
value: string;
|
|
48
|
+
}
|
|
49
|
+
export type TweakConfig = NumberTweakItem | BooleanTweakItem | TextTweakItem | ColorTweakItem | ImageTweakItem;
|
|
50
|
+
export interface TweaksConfig {
|
|
51
|
+
[key: string]: TweakConfig;
|
|
52
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
function isIOSDevice() {
|
|
3
|
+
const userAgent = navigator.userAgent;
|
|
4
|
+
if (/iPad|iPhone|iPod/.test(userAgent)) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
function isMediaStreamAudioSupported() {
|
|
13
|
+
try {
|
|
14
|
+
if (!window.AudioContext) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const tempContext = new AudioContext();
|
|
18
|
+
const hasMethod = typeof tempContext.createMediaStreamDestination === "function";
|
|
19
|
+
tempContext.close();
|
|
20
|
+
return hasMethod;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function createHiddenMediaElement(type = "video", debug = false) {
|
|
26
|
+
const element = document.createElement(type);
|
|
27
|
+
element.muted = false;
|
|
28
|
+
element.autoplay = true;
|
|
29
|
+
if (type === "video") {
|
|
30
|
+
element.playsInline = true;
|
|
31
|
+
}
|
|
32
|
+
if (debug) {
|
|
33
|
+
element.style.cssText = "position:fixed;bottom:10px;right:10px;width:200px;background:#ff0000;z-index:9999;";
|
|
34
|
+
} else {
|
|
35
|
+
element.style.cssText = "position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
36
|
+
}
|
|
37
|
+
return element;
|
|
38
|
+
}
|
|
39
|
+
function createHiddenVideoElement(debug = false) {
|
|
40
|
+
return createHiddenMediaElement("video", debug);
|
|
41
|
+
}
|
|
42
|
+
function patchAudioContext(audioContext, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
forceEnable = false,
|
|
45
|
+
autoCleanup = true,
|
|
46
|
+
debug = false,
|
|
47
|
+
mediaElementType = "video"
|
|
48
|
+
} = options;
|
|
49
|
+
const needsPatch = forceEnable || isIOSDevice();
|
|
50
|
+
if (!needsPatch) {
|
|
51
|
+
return Object.assign(audioContext, {
|
|
52
|
+
unlock: async () => {
|
|
53
|
+
if (audioContext.state === "suspended") {
|
|
54
|
+
await audioContext.resume();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
cleanup: () => {
|
|
58
|
+
},
|
|
59
|
+
isPatched: false,
|
|
60
|
+
originalDestination: audioContext.destination
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!isMediaStreamAudioSupported()) {
|
|
64
|
+
console.warn(
|
|
65
|
+
"[AudioContext] MediaStreamAudioDestinationNode not supported, falling back to native"
|
|
66
|
+
);
|
|
67
|
+
return Object.assign(audioContext, {
|
|
68
|
+
unlock: async () => audioContext.resume(),
|
|
69
|
+
cleanup: () => {
|
|
70
|
+
},
|
|
71
|
+
isPatched: false,
|
|
72
|
+
originalDestination: audioContext.destination
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const originalDestination = audioContext.destination;
|
|
76
|
+
const streamDestination = audioContext.createMediaStreamDestination();
|
|
77
|
+
const mediaElement = createHiddenMediaElement(mediaElementType, debug);
|
|
78
|
+
mediaElement.srcObject = streamDestination.stream;
|
|
79
|
+
document.body.appendChild(mediaElement);
|
|
80
|
+
Object.defineProperty(audioContext, "destination", {
|
|
81
|
+
get: () => streamDestination,
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true
|
|
84
|
+
});
|
|
85
|
+
if (!("maxChannelCount" in streamDestination)) {
|
|
86
|
+
Object.defineProperty(streamDestination, "maxChannelCount", {
|
|
87
|
+
get: () => originalDestination.maxChannelCount,
|
|
88
|
+
enumerable: true
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
let isUnlocked = false;
|
|
92
|
+
const unlock = async () => {
|
|
93
|
+
if (isUnlocked) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await mediaElement.play();
|
|
98
|
+
if (audioContext.state === "suspended") {
|
|
99
|
+
await audioContext.resume();
|
|
100
|
+
}
|
|
101
|
+
isUnlocked = true;
|
|
102
|
+
if (debug) {
|
|
103
|
+
console.log("[AudioContext] iOS unlock successful");
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("[AudioContext] Unlock failed:", error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
try {
|
|
112
|
+
mediaElement.pause();
|
|
113
|
+
mediaElement.srcObject = null;
|
|
114
|
+
mediaElement.remove();
|
|
115
|
+
if (debug) {
|
|
116
|
+
console.log("[AudioContext] Cleanup completed");
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("[AudioContext] Cleanup error:", error);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (autoCleanup) {
|
|
123
|
+
const originalClose = audioContext.close.bind(audioContext);
|
|
124
|
+
audioContext.close = async () => {
|
|
125
|
+
cleanup();
|
|
126
|
+
return originalClose();
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return Object.assign(audioContext, {
|
|
130
|
+
unlock,
|
|
131
|
+
cleanup,
|
|
132
|
+
isPatched: true,
|
|
133
|
+
originalDestination
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function useAudioContext(options = {}) {
|
|
137
|
+
const { autoUnlock = true, ...patchOptions } = options;
|
|
138
|
+
const audioContextRef = useRef(null);
|
|
139
|
+
const isUnlockedRef = useRef(false);
|
|
140
|
+
const unlockFnRef = useRef(null);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const ctx = new AudioContext();
|
|
143
|
+
audioContextRef.current = patchAudioContext(ctx, patchOptions);
|
|
144
|
+
return () => {
|
|
145
|
+
audioContextRef.current?.cleanup();
|
|
146
|
+
audioContextRef.current?.close();
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
if (!unlockFnRef.current) {
|
|
150
|
+
unlockFnRef.current = async () => {
|
|
151
|
+
const ctx = audioContextRef.current;
|
|
152
|
+
if (!ctx || isUnlockedRef.current) return;
|
|
153
|
+
try {
|
|
154
|
+
await ctx.unlock();
|
|
155
|
+
isUnlockedRef.current = true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.warn("Failed to unlock audio:", error);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!autoUnlock) return;
|
|
163
|
+
const handleInteraction = async () => {
|
|
164
|
+
await unlockFnRef.current?.();
|
|
165
|
+
};
|
|
166
|
+
document.addEventListener("click", handleInteraction, { once: true });
|
|
167
|
+
document.addEventListener("touchstart", handleInteraction, { once: true });
|
|
168
|
+
return () => {
|
|
169
|
+
document.removeEventListener("click", handleInteraction);
|
|
170
|
+
document.removeEventListener("touchstart", handleInteraction);
|
|
171
|
+
};
|
|
172
|
+
}, [autoUnlock]);
|
|
173
|
+
return {
|
|
174
|
+
audioContext: audioContextRef.current,
|
|
175
|
+
isUnlocked: isUnlockedRef.current,
|
|
176
|
+
unlock: unlockFnRef.current
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
createHiddenVideoElement as a,
|
|
181
|
+
isMediaStreamAudioSupported as b,
|
|
182
|
+
createHiddenMediaElement as c,
|
|
183
|
+
isIOSDevice as i,
|
|
184
|
+
patchAudioContext as p,
|
|
185
|
+
useAudioContext as u
|
|
186
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aippy/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.1",
|
|
4
4
|
"description": "Aippy Runtime SDK - Runtime SDK for Aippy projects",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"./tweaks": {
|
|
28
28
|
"import": "./dist/tweaks/index.js",
|
|
29
29
|
"types": "./dist/tweaks/index.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./audio": {
|
|
32
|
+
"import": "./dist/audio/index.js",
|
|
33
|
+
"types": "./dist/audio/index.d.ts"
|
|
30
34
|
}
|
|
31
35
|
},
|
|
32
36
|
"files": [
|