Skip to content

Commit 3e45fc5

Browse files
committed
Add background audio methods and types
1 parent 7db997d commit 3e45fc5

4 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ namespace Median {
4646
export const auth = plugins.auth;
4747
export const auth0 = plugins.auth0;
4848
export const autorefresh = plugins.autorefresh;
49+
export const backgroundAudio = plugins.backgroundAudio;
4950
export const backgroundLocation = plugins.backgroundLocation;
5051
export const backgroundMedia = plugins.backgroundMedia;
5152
export const barcode = plugins.barcode;
@@ -175,5 +176,6 @@ export default Median;
175176
// Types //
176177
///////////////////////////////
177178
export { AppsFlyer } from './types/appsflyer.js';
179+
export { BackgroundAudio } from './types/backgroundAudio.js';
178180
export { HealthBridge } from './types/healthBridge.js';
179181
export { MasterLock } from './types/masterlock.js';

src/plugins/backgroundAudio.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { BackgroundAudio } from '../types/backgroundAudio.js';
2+
import { addCallbackFunction, addCommand, addCommandCallback } from '../utils/index.js';
3+
4+
type RecordingStopInternalResponse = {
5+
success: boolean;
6+
base64?: string;
7+
mimeType?: string;
8+
state?: BackgroundAudio.RecordingState;
9+
durationSeconds?: number;
10+
transcript?: string;
11+
};
12+
13+
type ConvertResultParams = RecordingStopInternalResponse & {
14+
base64?: string;
15+
mimeType?: string;
16+
};
17+
18+
const backgroundAudio = {
19+
checkPermission: function () {
20+
return addCommandCallback<BackgroundAudio.PermissionResponse>('median://backgroundAudio/checkPermission');
21+
},
22+
requestPermission: function () {
23+
return addCommandCallback<BackgroundAudio.PermissionResponse>('median://backgroundAudio/requestPermission');
24+
},
25+
startRecording: function (params: BackgroundAudio.RecordingStartParams) {
26+
let onRecordingStop: string | undefined;
27+
28+
if (params?.onRecordingStop) {
29+
const onRecordingStopCallback = params.onRecordingStop;
30+
31+
const callback = function (result: RecordingStopInternalResponse) {
32+
try {
33+
const data = median_background_audio_convert_result(result);
34+
onRecordingStopCallback(data);
35+
} catch (error) {
36+
onRecordingStopCallback({ success: false, error });
37+
}
38+
};
39+
40+
onRecordingStop = addCallbackFunction(callback);
41+
}
42+
43+
return addCommandCallback<BackgroundAudio.RecordingStartResponse>('median://backgroundAudio/startRecording', {
44+
...params,
45+
onRecordingStop,
46+
});
47+
},
48+
stopRecording: async function () {
49+
const result = await addCommandCallback<RecordingStopInternalResponse>('median://backgroundAudio/stopRecording');
50+
return median_background_audio_convert_result(result);
51+
},
52+
pause: function () {
53+
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/pause');
54+
},
55+
resume: function () {
56+
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/resume');
57+
},
58+
getStatus: function () {
59+
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/getStatus');
60+
},
61+
addListener: async function (callback: (data: BackgroundAudio.RecordingEvent) => void) {
62+
const listenerIdCallback = addCallbackFunction(callback, true);
63+
const result = await addCommandCallback('median://backgroundAudio/addListener', { listenerIdCallback });
64+
if (!result?.listenerId) {
65+
throw 'INVALID_LISTENER_ID';
66+
} else {
67+
return result.listenerId;
68+
}
69+
},
70+
removeListener: function (listenerId: string) {
71+
addCommand('median://backgroundAudio/removeListener', { listenerId });
72+
},
73+
};
74+
75+
function median_background_audio_convert_result(result: ConvertResultParams): BackgroundAudio.RecordingStopResponse {
76+
if (result?.success && result?.base64 && result?.mimeType) {
77+
const fileUri = median_background_audio_base64_to_url(result.base64, result.mimeType);
78+
79+
return {
80+
success: result.success,
81+
fileUri,
82+
state: result.state,
83+
durationSeconds: result.durationSeconds,
84+
transcript: result.transcript,
85+
};
86+
}
87+
88+
return result;
89+
}
90+
91+
function median_background_audio_base64_to_url(base64: string, mimeType: string) {
92+
const byteChars = atob(base64);
93+
const byteNumbers = new Array(byteChars.length);
94+
for (let i = 0; i < byteChars.length; i++) {
95+
byteNumbers[i] = byteChars.charCodeAt(i);
96+
}
97+
const byteArray = new Uint8Array(byteNumbers);
98+
const blob = new Blob([byteArray], { type: mimeType });
99+
return URL.createObjectURL(blob);
100+
}
101+
102+
export default backgroundAudio;

src/plugins/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { default as appsflyer } from './appsflyer.js';
66
export { default as auth } from './auth.js';
77
export { default as auth0 } from './auth0.js';
88
export { default as autorefresh } from './autorefresh.js';
9+
export { default as backgroundAudio } from './backgroundAudio.js';
910
export { default as backgroundLocation } from './backgroundLocation.js';
1011
export { default as backgroundMedia } from './backgroundMedia.js';
1112
export { default as barcode } from './barcode.js';

src/types/backgroundAudio.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export namespace BackgroundAudio {
2+
export type PermissionResponse = {
3+
granted: boolean;
4+
status: 'granted' | 'denied' | 'not-determined' | 'restricted';
5+
};
6+
7+
export type RecordingState = 'idle' | 'recording' | 'paused' | 'stopped' | 'error';
8+
9+
export type RecordingStartParams = {
10+
/** Audio format for the recorded file. Default: 'm4a'. */
11+
format?: 'm4a' | 'wav';
12+
/** Maximum recording duration in seconds. Default: 3600 (60 min). */
13+
maxDuration?: number;
14+
/** Enable on-device speech-to-text transcription. Default: false. */
15+
enableTranscription?: boolean;
16+
/** Locale/language code for on-device STT (e.g. 'en-US'). Default: device locale. */
17+
sttLanguage?: string;
18+
/** Callback when max duration is hit. */
19+
onRecordingStop?: (data: RecordingStopResponse) => void;
20+
};
21+
22+
export type RecordingStartResponse = {
23+
success: boolean;
24+
state: RecordingState;
25+
};
26+
27+
export type RecordingStopResponse = {
28+
success: boolean;
29+
error?: string | unknown;
30+
state?: RecordingState;
31+
/** Local file URI of the recorded audio. */
32+
fileUri?: string;
33+
/** Duration of the recording in seconds. */
34+
durationSeconds?: number;
35+
/** Transcript text (populated only when enableTranscription is true). */
36+
transcript?: string;
37+
};
38+
39+
export type RecordingStatusResponse = {
40+
state: RecordingState;
41+
elapsedSeconds?: number;
42+
};
43+
44+
export type AddListenerResponse = {
45+
listenerId: string;
46+
};
47+
48+
export type RecordingEvent = {
49+
state: RecordingState;
50+
durationSeconds?: number;
51+
};
52+
}

0 commit comments

Comments
 (0)