/** * * Agora Real Time Engagement * Created by Wei Hu in 2021-09. * Copyright (c) 2022 Agora IO. All rights reserved. * */ import * as path from 'path'; import { deleteRte } from '../addon/export_rte'; import * as fs from '../common/fs'; import * as log from '../common/logger'; import { MetadataInfo, MetadataType } from '../metadata/metadata'; import { Cmd } from '../msg/cmd/cmd'; import { ImageFrame } from '../msg/image_frame/image_frame'; import { PcmFrame } from '../msg/pcm_frame/pcm_frame'; import { RTE } from '../rte/rte'; import rte_addon from '../rte_addon'; export abstract class Extension { constructor(name: string) { this.name = name; const callsites = fs.callsites(); log.debug(`=======The callstacks of Extension[${name}]=======`); for (let i = 0; i < callsites.length; i++) { log.debug(`i = ${i}, `, callsites[i].getFileName()); } log.debug(`=======The callstacks of Extension[${name}]=======`); if (callsites.length <= 2) { throw new Error( 'The size of call stacks in the extension initialization should >= 3', ); } // The user-defined-extension will be loaded in dynamic or static way. // // - dynamic // - The extension is defined with '@Addon' decorator, for example: // // @Addon('demo') // export class DemoExtension extends Extension {} // // And the extension info in the graph will be like: // { // "addon": "demo", // "extension": "demo" // } // // - In the dynamic way, the entry point will be // 'Extension::createInstance' below. // // - static // - The extension is defined _without_ '@Addon' decorator, and the // extension instance is created in a ExtensionGroup, for example: // // class NodeTestGroup extends ExtensionGroup { // override async onCreateExtension(): Promise { // const demo = new DemoExtension('demo'); // return [ demo ]; // } // } // // - In the static way, the entry point will be // 'ExtensionGroup::onCreateExtension'. // let entryPoint = 0; for (; entryPoint < callsites.length; entryPoint++) { // The next of the entry point in the static way is null. if (!callsites[entryPoint] || !callsites[entryPoint].getFileName()) { break; } } // The user-defined-extension must be a subclass of the base Extension, and // the inheritance chain may contain multilevel. For example: // // - class A extends Extension, the call chain will be: // - Extension's constructor // - A's constructor // - runtime_nodejs (the 'Extension::createInstance' will always be the // first stack frame in the JavaScript call chain) // // - class A extends B extends C ... extends Extension // - Extension's constructor // - C's constructor // - B's // - A's // - Extension::createInstance // // So, the user-defined-extension will always be the next to last. if (entryPoint >= 2 && callsites[entryPoint - 2]) { const extensionName = callsites[entryPoint - 2].getFileName(); if (extensionName) { this.addonFolder = `${path.dirname(extensionName)}/../`; log.debug(`The root dir of Extension '${name}' is ${this.addonFolder}`); } } rte_addon.rte_nodejs_extension_create(this, name); } static onInit: | undefined | (( rte: RTE, manifest: MetadataInfo, property: MetadataInfo ) => Promise) = undefined; static createInstance(name: string): Extension { if (this === Extension) { throw new Error( `'createInstance' method must be implemented in derived classes, \ check instance ${name}!`, ); } return new (this as any)(name); } // eslint-disable-next-line @typescript-eslint/no-empty-function static destroyInstance(_extension: Extension): void {} private async _onInit( rte: RTE, manifest: MetadataInfo, property: MetadataInfo, ): Promise { try { await this.onInit(rte, manifest, property); } catch (e) { log.error(`Extension '${this.name}' onInit throws an exception: ${e}`); } finally { // Notify RTE runtime that extension::onInit is completed. rte_addon.rte_nodejs_rte_on_init_done(rte, manifest, property); } } // The manifest.json is the extension's metadata, but not the json // schema now. So we can not set default manifest now. // // TODO(Liu): divide the content of manifest.json into two parts --- // 'metadata' and 'schema'. private async defaultOnInit(property: MetadataInfo): Promise { if (this.addonFolder) { const propFile = `${this.addonFolder}/property.json`; const cachedPropFile = `${this.addonFolder}/${this.name}_property.json`; if (fs.fileExistsSync(propFile)) { this.inittedByDefault = true; const rawPropStr = await fs.readFile(propFile); const rawProp = JSON.parse(rawPropStr); if (fs.fileExistsSync(cachedPropFile)) { const cachedPropStr = await fs.readFile(cachedPropFile); const cachedProp = JSON.parse(cachedPropStr); const mergedProp = { ...rawProp, ...cachedProp }; property.set(MetadataType.JSON_STR, JSON.stringify(mergedProp)); } else { property.set(MetadataType.JSON_STR, rawPropStr); } } } } async onInit( _rte: RTE, _manifest: MetadataInfo, property: MetadataInfo, ): Promise { await this.defaultOnInit(property); } private async _onStart(rte: RTE): Promise { try { await this.onStart(rte); } catch (e) { log.error(`Extension '${this.name}' onStart throws an exception: ${e}`); } finally { // Notify RTE runtime that extension::onStart is completed. rte_addon.rte_nodejs_rte_on_start_done(rte); } } async onStart(_rte: RTE): Promise { // stub for override } private async _onStop(rte: RTE): Promise { try { await this.onStop(rte); if (this.inittedByDefault) { await this.defaultOnDeinit(rte); } } catch (e) { log.error(`Extension '${this.name}' onStop throws an exception: ${e}`); } finally { // Remove rte object from global object 'rtes'. deleteRte(rte); // Notify RTE runtime that extension::onStop is completed. rte_addon.rte_nodejs_rte_on_stop_done(rte); // JS extension prepare to be destroyed. rte_addon.rte_nodejs_extension_deinit(this); } } private async defaultOnDeinit(rte: RTE): Promise { const dirtyPropStr = rte.dumpDirtyProp(); if (!dirtyPropStr || dirtyPropStr === '') { return; } const dirtyProp = JSON.parse(dirtyPropStr); const cachedPropFile = `${this.addonFolder}/${this.name}_property.json`; if (fs.fileExistsSync(cachedPropFile)) { const cachedPropStr = await fs.readFile(cachedPropFile); const cachedProp = JSON.parse(cachedPropStr); const mergedProp = { ...cachedProp, ...dirtyProp }; await fs.writeFile(cachedPropFile, JSON.stringify(mergedProp)); } else { if (!fs.fileExistsSync(`${this.addonFolder}`)) { await fs.mkdir(`${this.addonFolder}`); } await fs.writeFile(cachedPropFile, dirtyPropStr); } } async onStop(_rte: RTE): Promise { // stub for override } private async _onCmd(rte: RTE, cmd: Cmd): Promise { try { await this.onCmd(rte, cmd); } catch (e) { log.error(`Extension '${this.name}' onCmd throws an exception: ${e}`); } finally { // TODO(Liu): It is better to add some extra information: // - execution result and error messages // - timestamp rte_addon.rte_nodejs_rte_on_cmd_done( rte, cmd.cmd_id as string, cmd.command, ); } } async onCmd(_rte: RTE, _cmd: Cmd): Promise { // stub for override } private async _onData(rte: RTE, data: ArrayBuffer): Promise { try { await this.onData(rte, data); } catch (e) { log.error(`Extension '${this.name}' onData throws an exception: ${e}`); } } async onData(_rte: RTE, _data: ArrayBuffer): Promise { // stub for override } private async _onImageFrame(rte: RTE, frame: ImageFrame): Promise { try { await this.onImageFrame(rte, frame); } catch (e) { log.error( `Extension '${this.name}' onImageFrame throws an exception: ${e}`, ); } } async onImageFrame(_rte: RTE, _frame: ImageFrame): Promise { // stub for override } private async _onPcmFrame(rte: RTE, frame: PcmFrame): Promise { try { await this.onPcmFrame(rte, frame); } catch (e) { log.error( `Extension '${this.name}' onPcmFrame throws an exception: ${e}`, ); } } async onPcmFrame(_rte: RTE, _frame: PcmFrame): Promise { // stub for override } name: string; addonFolder: string | undefined; private inittedByDefault = false; }