Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ namespace Median {
export const auth = plugins.auth;
export const auth0 = plugins.auth0;
export const autorefresh = plugins.autorefresh;
export const backgroundAudio = plugins.backgroundAudio;
export const backgroundLocation = plugins.backgroundLocation;
export const backgroundMedia = plugins.backgroundMedia;
export const barcode = plugins.barcode;
Expand Down Expand Up @@ -175,5 +176,6 @@ export default Median;
// Types //
///////////////////////////////
export { AppsFlyer } from './types/appsflyer.js';
export { BackgroundAudio } from './types/backgroundAudio.js';
export { HealthBridge } from './types/healthBridge.js';
export { MasterLock } from './types/masterlock.js';
102 changes: 102 additions & 0 deletions src/plugins/backgroundAudio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { BackgroundAudio } from '../types/backgroundAudio.js';
import { addCallbackFunction, addCommand, addCommandCallback } from '../utils/index.js';

type RecordingStopInternalResponse = {
success: boolean;
base64?: string;
mimeType?: string;
state?: BackgroundAudio.RecordingState;
durationSeconds?: number;
transcript?: string;
};

type ConvertResultParams = RecordingStopInternalResponse & {
base64?: string;
mimeType?: string;
};

const backgroundAudio = {
checkPermission: function () {
return addCommandCallback<BackgroundAudio.PermissionResponse>('median://backgroundAudio/checkPermission');
},
requestPermission: function () {
return addCommandCallback<BackgroundAudio.PermissionResponse>('median://backgroundAudio/requestPermission');
},
startRecording: function (params: BackgroundAudio.RecordingStartParams) {
let onRecordingStop: string | undefined;

if (params?.onRecordingStop) {
const onRecordingStopCallback = params.onRecordingStop;

const callback = function (result: RecordingStopInternalResponse) {
try {
const data = median_background_audio_convert_result(result);
onRecordingStopCallback(data);
} catch (error) {
onRecordingStopCallback({ success: false, error });
}
};

onRecordingStop = addCallbackFunction(callback);
}

return addCommandCallback<BackgroundAudio.RecordingStartResponse>('median://backgroundAudio/startRecording', {
...params,
onRecordingStop,
});
},
stopRecording: async function () {
const result = await addCommandCallback<RecordingStopInternalResponse>('median://backgroundAudio/stopRecording');
return median_background_audio_convert_result(result);
},
pause: function () {
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/pause');
},
resume: function () {
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/resume');
},
getStatus: function () {
return addCommandCallback<BackgroundAudio.RecordingStatusResponse>('median://backgroundAudio/getStatus');
},
addListener: async function (callback: (data: BackgroundAudio.RecordingEvent) => void) {
const listenerIdCallback = addCallbackFunction(callback, true);
const result = await addCommandCallback('median://backgroundAudio/addListener', { listenerIdCallback });
if (!result?.listenerId) {
throw 'INVALID_LISTENER_ID';
} else {
return result.listenerId;
}
},
removeListener: function (listenerId: string) {
addCommand('median://backgroundAudio/removeListener', { listenerId });
},
};

function median_background_audio_convert_result(result: ConvertResultParams): BackgroundAudio.RecordingStopResponse {
if (result?.success && result?.base64 && result?.mimeType) {
const fileUri = median_background_audio_base64_to_url(result.base64, result.mimeType);

return {
success: result.success,
fileUri,
state: result.state,
durationSeconds: result.durationSeconds,
transcript: result.transcript,
};
}

return result;
}

function median_background_audio_base64_to_url(base64: string, mimeType: string) {
const byteChars = atob(base64);
const byteNumbers = new Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteNumbers[i] = byteChars.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: mimeType });
return URL.createObjectURL(blob);
}

export default backgroundAudio;
1 change: 1 addition & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as appsflyer } from './appsflyer.js';
export { default as auth } from './auth.js';
export { default as auth0 } from './auth0.js';
export { default as autorefresh } from './autorefresh.js';
export { default as backgroundAudio } from './backgroundAudio.js';
export { default as backgroundLocation } from './backgroundLocation.js';
export { default as backgroundMedia } from './backgroundMedia.js';
export { default as barcode } from './barcode.js';
Expand Down
52 changes: 52 additions & 0 deletions src/types/backgroundAudio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export namespace BackgroundAudio {
export type PermissionResponse = {
granted: boolean;
status: 'granted' | 'denied' | 'not-determined' | 'restricted';
};

export type RecordingState = 'idle' | 'recording' | 'paused' | 'stopped' | 'error';

export type RecordingStartParams = {
/** Audio format for the recorded file. Default: 'm4a'. */
format?: 'm4a' | 'wav';
/** Maximum recording duration in seconds. Default: 3600 (60 min). */
maxDuration?: number;
/** Enable on-device speech-to-text transcription. Default: false. */
enableTranscription?: boolean;
/** Locale/language code for on-device STT (e.g. 'en-US'). Default: device locale. */
sttLanguage?: string;
/** Callback when max duration is hit. */
onRecordingStop?: (data: RecordingStopResponse) => void;
};

export type RecordingStartResponse = {
success: boolean;
state: RecordingState;
};

export type RecordingStopResponse = {
success: boolean;
error?: string | unknown;
state?: RecordingState;
/** Local file URI of the recorded audio. */
fileUri?: string;
/** Duration of the recording in seconds. */
durationSeconds?: number;
/** Transcript text (populated only when enableTranscription is true). */
transcript?: string;
};

export type RecordingStatusResponse = {
state: RecordingState;
elapsedSeconds?: number;
};

export type AddListenerResponse = {
listenerId: string;
};

export type RecordingEvent = {
state: RecordingState;
durationSeconds?: number;
};
}