feat(editor): audio block (#10947)

AudioMedia entity for loading & controlling a single audio media
AudioMediaManagerService: Global audio state synchronization across tabs
AudioAttachmentService + AudioAttachmentBlock for manipulating AttachmentBlock in affine - e.g., filling transcription (using mock endpoint for now)
Added AudioBlock + AudioPlayer for rendering audio block in affine (new transcription block whose renderer is provided in affine)

fix AF-2292
fix AF-2337
This commit is contained in:
pengx17
2025-03-20 12:46:14 +00:00
parent 8a5393ea50
commit fad49bb070
120 changed files with 5407 additions and 950 deletions
+1
View File
@@ -0,0 +1 @@
export * from './types';
+72
View File
@@ -0,0 +1,72 @@
/**
* Attachment block audio media.
* blockId/docId/workspaceId are used to identify the source of the media
* to control the exclusivity playback state of audio across the whole application.
*/
export interface AttachmentBlockAudioMedia {
blobId: string; // aka sourceId
blockId: string;
docId: string;
workspaceId: string;
}
export interface AudioMediaDescriptor {
key: AudioMediaKey;
tabId: string | null;
name: string;
size: number;
blobId: string; // aka sourceId
}
// workspaceId/docId/blockId/blobId
export type AudioMediaKey = `${string}/${string}/${string}/${string}`;
export const attachmentBlockAudioMediaKey = (
media: AttachmentBlockAudioMedia
): AudioMediaKey => {
return `${media.workspaceId}/${media.docId}/${media.blockId}/${media.blobId}`;
};
export const parseAudioMediaKey = (
key: AudioMediaKey
): AttachmentBlockAudioMedia => {
const [workspaceId, docId, blockId, blobId] = key.split('/');
return {
workspaceId,
docId,
blockId,
blobId,
};
};
type State = 'idle' | 'playing' | 'paused' | 'stopped';
export interface MediaStats {
key: AudioMediaKey;
tabId: string | null;
duration: number;
name: string;
size: number;
waveform: number[]; // for drawing waveform, maxmium of 1000 samples
}
export interface PlaybackState {
key: AudioMediaKey;
tabId: string | null;
state: State;
/**
* Whenever the user seek the media, the startSeekOffset will be updated
*/
seekOffset: number;
/**
* Whenever the media state is updated.
* the updateTime will be updated. It is in milliseconds (unix timestamp).
* The current playback position (0-based, in seconds) is calculated by `seekOffset + (Date.now() - updateTime) / 1000 * rate`
*/
updateTime: number;
/**
* the playback rate
* Not implemented yet. Always 1.0.
*/
// rate: number;
}