Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(service-providers): check recording end status #2146

Merged
merged 1 commit into from
Jun 11, 2024
Merged
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
1 change: 1 addition & 0 deletions packages/flat-i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
"user-guide-button": "Check it out now",
"start-recording": "Start recording",
"stop-recording": "Stop recording",
"recording-end": "Recording ended",
"recording": "Recording",
"open-in-browser": "Open in Browser",
"room-started": "Started: ",
Expand Down
1 change: 1 addition & 0 deletions packages/flat-i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
"user-guide-button": "立即查看",
"start-recording": "开始录制",
"stop-recording": "停止录制",
"recording-end": "录制已停止",
"recording": "录制",
"open-in-browser": "请在浏览器中打开",
"room-started": "已上课:",
Expand Down
3 changes: 3 additions & 0 deletions packages/flat-services/src/services/recording/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export abstract class IServiceRecording implements IService {
/** Use with try-catch. */
public abstract startRecording(): Promise<void>;

/** Refresh `isRecording` state. */
public abstract checkIsRecording(): Promise<void>;

/** Use with try-catch. */
public abstract stopRecording(): Promise<void>;

Expand Down
33 changes: 32 additions & 1 deletion packages/flat-stores/src/classroom-store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export class ClassroomStore {
private readonly sideEffect = new SideEffectManager();
private readonly rewardCooldown = new Map<string, number>();

// If it is `true`, the stop-recording is triggered by the user, do not show the message.
private recordingEndSentinel = false;

public readonly roomUUID: string;
public readonly ownerUUID: string;
/** User uuid of the current user */
Expand Down Expand Up @@ -181,11 +184,12 @@ export class ClassroomStore {
onDrop: this.onDrop,
});

makeAutoObservable<this, "sideEffect" | "rewardCooldown">(this, {
makeAutoObservable<this, "sideEffect" | "rewardCooldown" | "recordingEndSentinel">(this, {
rtc: observable.ref,
rtm: observable.ref,
sideEffect: false,
rewardCooldown: false,
recordingEndSentinel: false,
deviceStateStorage: false,
classroomStorage: false,
onStageUsersStorage: false,
Expand All @@ -206,6 +210,8 @@ export class ClassroomStore {
(isRecording: boolean) => {
if (isRecording) {
void message.success(FlatI18n.t("start-recording"));
} else if (!this.recordingEndSentinel) {
void message.info(FlatI18n.t("recording-end"));
}
},
),
Expand Down Expand Up @@ -777,6 +783,29 @@ export class ClassroomStore {
user.camera = false;
}
});
// When there's no active stream in the channel, the recording service
// stops automatically after `maxIdleTime`.
if (this.isRecording) {
let hasStream = false;
for (const userUUID in deviceStateStorage.state) {
const deviceState = deviceStateStorage.state[userUUID];
if (deviceState.camera || deviceState.mic) {
hasStream = true;
break;
}
}
if (!hasStream) {
this.sideEffect.setTimeout(
() => {
if (this.isRecording) {
this.recording.checkIsRecording().catch(console.warn);
}
},
// Roughly 5 minutes later, see cloud-recording.ts.
5 * 61 * 1000,
);
}
}
}),
);

Expand Down Expand Up @@ -1430,7 +1459,9 @@ export class ClassroomStore {
}

private async stopRecording(): Promise<void> {
this.recordingEndSentinel = true;
await this.recording.stopRecording();
this.recordingEndSentinel = false;
}

private async initRTC(): Promise<void> {
Expand Down
21 changes: 20 additions & 1 deletion service-providers/agora-cloud-recording/src/cloud-recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type AgoraCloudRecordingRoomInfo = IServiceRecordingJoinRoomConfig;
// TODO: We should save the recording state at the server side.
export class AgoraCloudRecording extends IServiceRecording {
private static readonly ReportingEndTimeKey = "reportingEndTime";
private static readonly LoopQueryKey = "loopQuery";

private roomInfo: AgoraCloudRecordingRoomInfo | null;
private recordingState: AgoraCloudRecordingState | null;
Expand Down Expand Up @@ -132,6 +133,7 @@ export class AgoraCloudRecording extends IServiceRecording {
const { roomID } = this.roomInfo;
const { resourceId, sid, mode } = this.recordingState;

this.sideEffect.flush(AgoraCloudRecording.LoopQueryKey);
this.sideEffect.flush(AgoraCloudRecording.ReportingEndTimeKey);
this.recordingState = null;

Expand Down Expand Up @@ -170,6 +172,10 @@ export class AgoraCloudRecording extends IServiceRecording {
});
}

public async checkIsRecording(): Promise<void> {
await this.queryRecordingStatus();
}

private async queryRecordingStatus(joinRoom = false): Promise<void> {
if (this.recordingState === null || this.roomID === null) {
this.$Val.isRecording$.setValue(false);
Expand Down Expand Up @@ -212,12 +218,25 @@ export class AgoraCloudRecording extends IServiceRecording {
10 * 1000,
AgoraCloudRecording.ReportingEndTimeKey,
);
// Loop query status in each 2 minutes.
// https://doc.shengwang.cn/doc/cloud-recording/restful/best-practices/integration#%E5%91%A8%E6%9C%9F%E6%80%A7%E9%A2%91%E9%81%93%E6%9F%A5%E8%AF%A2
this.sideEffect.setInterval(
() => {
if (this.isRecording) {
this.queryRecordingStatus();
} else {
this.sideEffect.flush(AgoraCloudRecording.LoopQueryKey);
}
},
120 * 1000,
AgoraCloudRecording.LoopQueryKey,
);
}
}

/**
* @see {@link https://docs.agora.io/en/cloud-recording/reference/rest-api/rest}
* @see {@link https://docs.agora.io/cn/cloud-recording/cloud_recording_api_rest}
* @see {@link https://doc.shengwang.cn/doc/cloud-recording/restful/landing-page}
*/
export type AgoraCloudRecordingMode = "individual" | "mix";

Expand Down
Loading