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

View File

@@ -2,6 +2,7 @@ export * from './app-config-storage';
export * from './atom';
export * from './framework';
export * from './livedata';
export * from './media';
export * from './orm';
export * from './storage';
export * from './sync';

View File

@@ -0,0 +1 @@
export * from './types';

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;
}

View File

@@ -1,6 +1,6 @@
import { Unreachable } from '@affine/env/constant';
export interface RcRef<T> {
export interface RcRef<T> extends Disposable {
obj: T;
release: () => void;
}
@@ -21,17 +21,19 @@ export class ObjectPool<Key, T> {
if (exist) {
exist.rc++;
let released = false;
const release = () => {
// avoid double release
if (released) {
return;
}
released = true;
exist.rc--;
this.requestGc();
};
return {
obj: exist.obj,
release: () => {
// avoid double release
if (released) {
return;
}
released = true;
exist.rc--;
this.requestGc();
},
release,
[Symbol.dispose]: release,
};
}
return null;