Skip to content

Commit

Permalink
feat(audio-rec): add recording timestamp
Browse files Browse the repository at this point in the history
close #249
  • Loading branch information
aidenlx committed Mar 19, 2024
1 parent 41e2942 commit db5cca5
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 5 deletions.
234 changes: 234 additions & 0 deletions apps/app/src/audio-rec/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { around } from "monkey-around";
import type { App, Editor, TAbstractFile } from "obsidian";
import { MarkdownView, Component, Notice, TFile } from "obsidian";

import { mediaInfoFromFile } from "@/info/media-info";
import {
insertTimestamp,
timestampGenerator,
} from "@/media-note/timestamp/utils";
import type MediaExtended from "../mx-main";

export class RecorderNote extends Component {
constructor(public plugin: MediaExtended) {
super();
}

onload(): void {
if (!this.rec) {
console.info("recorder not found, skip patching for recorder note");
}
this.patch();
this.addCommand();
}

get app() {
return this.plugin.app;
}
get rec() {
return this.app.internalPlugins.plugins["audio-recorder"];
}
get settings() {
return this.plugin.settings.getState();
}

_recording: { start: number; notified?: boolean; end?: number } | null = null;
_recordedEditors = new Map<Editor, TFile | null>();

onunload(): void {
this._recordedEditors.clear();
}

async onRecordingSaved(file: TFile) {
if (!this._recording) return;
const { start } = this._recording;
this._recording = null;

const mediaInfo = mediaInfoFromFile(file, "");
if (!mediaInfo) {
new Notice("Failed to get media info from the saved file: " + file.path);
return;
}

for (const entry of this._recordedEditors) {
let editor: Editor | undefined = entry[0];
let close: () => void = () => void 0;
const noteFile = entry[1];
if (!editor.containerEl.isConnected) {
if (!noteFile) {
new Notice(
"One of the note with timestamp is closed, the timestamp will not be updated",
);
continue;
}
editor = findOpenedEditor(noteFile, this.app);
if (!editor) {
const leaf = this.app.workspace.getLeaf("tab");
await leaf.openFile(noteFile, { state: { mode: "source" } });
if (!(leaf.view instanceof MarkdownView)) {
new Notice(
"Failed to open note for timestamp update: " + noteFile.path,
);
continue;
}
editor = leaf.view.editor;
close = () => leaf.detach();
}
}
try {
const content = editor
.getValue()
.replaceAll(genPlaceholderPattern(start), (_, offsetStr) => {
const offsetMs = parseInt(offsetStr, 10);
const genTimestamp = timestampGenerator(offsetMs / 1e3, mediaInfo, {
app: this.app,
settings: this.plugin.settings.getState(),
});
return genTimestamp(noteFile?.path ?? "");
});
editor.setValue(content);
} catch (e) {
console.error("failed to insert timestamp", e);
} finally {
close();
}
}
this._recordedEditors.clear();
}

patch() {
if (!this.rec.instance) return;
const instance = this.rec.instance;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const { vault, workspace } = this.app;
this.register(
around(instance, {
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
saveRecording(next) {
return function (this: any, ...args: any[]) {
// prevent default behavior of opening new recording override and close the current editor
workspace
.getLeaf("split")
.setViewState({ type: "empty", active: true });
const returns = next.apply(this, args);
const off = () => vault.off("create", handler);
const timeout = window.setTimeout(off, 300e3); // wait for 5min
const handler = (file: TAbstractFile) => {
if (file instanceof TFile) {
self.onRecordingSaved(file);
} else {
console.error("unexpected folder");
}
off();
window.clearTimeout(timeout);
};
vault.on("create", handler);
return returns;
};
},

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
startRecording(next) {
return function (this: any, ...args: any[]) {
const re = next.apply(this, args);
const recorder: MediaRecorder | undefined = this.recorder;
self._recording = { start: Date.now() };
console.debug("recording start called", self._recording.start);
if (recorder && recorder instanceof MediaRecorder) {
recorder.addEventListener(
"start",
() => {
self._recording = { start: Date.now() };
console.debug(
"recording started in MediaRecorder",
self._recording.start,
);
},
{ once: true },
);
const recordStopTime = () => {
if (self._recording) {
self._recording.end = Date.now();
console.debug(
"recording stopped in MediaRecorder",
self._recording.end,
);
}
recorder.removeEventListener("stop", recordStopTime);
recorder.removeEventListener("error", recordStopTime);
};
recorder.addEventListener("stop", recordStopTime, { once: true });
recorder.addEventListener("error", recordStopTime, {
once: true,
});
}
return re;
};
},
}),
);
}

addCommand() {
this.plugin.addCommand({
id: "take-rec-timestamp",
name: "Take timestamp on current recording",
editorCheckCallback: (checking, editor, view) => {
if (!this._recording) return false;
if (checking) return true;
const { start, notified } = this._recording;
if (!notified && !view.file) {
new Notice(
"You've taken a timestamp for the recording, probably in canvas node, " +
"keep editor in foreground and in live preview mode. " +
"Otherwise, the dummy timestamp cannot be updated when recording is saved.",
);
this._recording.notified = true;
}
const timestamp = stringifyPlaceholder(start, Date.now() - start);
insertTimestamp(
{ timestamp },
{
editor,
template: this.settings.timestampTemplate,
insertBefore: this.settings.insertBefore,
},
);
this._recordedEditors.set(editor, view.file);
},
});
}
}

function genPlaceholderPattern(start: number) {
return new RegExp(`%%REC_${start}_(?<offset>\\d+)%%`, "g");
}
function stringifyPlaceholder(start: number, offset: number) {
return `%%REC_${start}_${offset}%%`;
}

declare module "obsidian" {
interface App {
internalPlugins: {
plugins: Record<string, any>;
};
}
interface Editor {
containerEl: HTMLElement;
}
}

function findOpenedEditor(file: TFile, app: App) {
let view: MarkdownView | null = null as any;
app.workspace.iterateAllLeaves((leaf) => {
if (
leaf.view instanceof MarkdownView &&
file.path === leaf.view.file?.path &&
leaf.view.getMode() === "source"
) {
view = leaf.view;
}
});
return view?.editor;
}
2 changes: 1 addition & 1 deletion apps/app/src/media-note/timestamp/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function takeTimestamp<T extends PlayerComponent>(
const genTimestamp = timestampGenerator(time, mediaInfo, {
app: playerComponent.plugin.app,
settings: playerComponent.plugin.settings.getState(),
state: player.state,
duration: player.state.duration,
});

if (time <= 0) {
Expand Down
10 changes: 6 additions & 4 deletions apps/app/src/media-note/timestamp/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { MediaState } from "@vidstack/react";
import { Notice } from "obsidian";
import type { App, Editor } from "obsidian";
import { isFileMediaInfo } from "@/info/media-info";
Expand Down Expand Up @@ -70,18 +69,21 @@ export function openOrCreateMediaNote(
}
}

/**
* @param time in seconds
*/
export function timestampGenerator(
time: number,
mediaInfo: MediaInfo,
{
app: { fileManager },
settings: { timestampOffset },
state: { duration },
}: { app: App; settings: MxSettings; state: Readonly<MediaState> },
duration = +Infinity,
}: { app: App; settings: MxSettings; duration?: number },
): (newNotePath: string) => string {
time += timestampOffset;
if (time < 0) time = 0;
if (duration && time > duration) time = duration;
else if (time > duration) time = duration;

const timeInDuration = formatDuration(time);
const frag =
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/mx-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./icons";

import type { PaneType, SplitDirection } from "obsidian";
import { Notice, Plugin } from "obsidian";
import { RecorderNote } from "./audio-rec";
import type { MediaInfo } from "./info/media-info";
import { MediaFileExtensions } from "./info/media-type";
import { URLViewType } from "./info/view-type";
Expand Down Expand Up @@ -100,6 +101,7 @@ export default class MxPlugin extends Plugin {
playlist = this.addChild(new PlaylistIndex(this));
biliReq = this.addChild(new BilibiliRequestHacker(this));
leafOpener = this.addChild(new LeafOpener(this));
recorderNote = this.addChild(new RecorderNote(this));
handleMediaNote = handleMediaNote;
injectMediaEmbed = injectMediaEmbed;
injectMediaView = injectMediaView;
Expand Down
1 change: 1 addition & 0 deletions apps/docs/pages/reference/commands.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All commands can be assigned to hotkeys in the settings. For more information on
- **Save screenshot**: Save a screenshot of the current media frame, and attach it to the current note.
- **Take timestamp in media note**: Capture the current playback time as a timestamp and add it to a media note.
- **Save screenshot in media note**: Save a screenshot of the current media frame and add it to a media note.
- **Take timestamp on current recording**: Capture the playback time for the current recording session. (* New in v3.1.0 *)

## Playback Speed Adjustment [#playback-speed]

Expand Down
1 change: 1 addition & 0 deletions apps/docs/pages/reference/commands.zh-CN.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- **Save screenshot**: 保存当前媒体的截图到指定位置,并插入当前的笔记
- **Take timestamp in media note**: 在媒体笔记中记录时间戳
- **Save screenshot in media note**: 在媒体笔记中保存当前媒体的截图,并插入当前的笔记
- **Take timestamp on current recording**: 在当前笔记中为正在进行的录音记录时间戳。(* v3.1.0+支持 *

## 倍速调节 [#playback-speed]

Expand Down

0 comments on commit db5cca5

Please sign in to comment.