# Welcome

This wiki serves as the comprehensive technical documentation for TheOpenPresenter system. Whether you're interested in understanding the system architecture or planning to contribute to its development, you'll find detailed information about the platform's inner workings here.

### Scope

This documentation is primarily intended for:

* Developers
* System administrators
* Technical contributors
* Those interested in extending TheOpenPresenter's functionality

> **Note**: If you're looking for user documentation or operating instructions, please note that these are not available yet and will be published separately as the software matures.

As an open-source project, we encourage community contributions and hope these resources will enable developers to effectively work with and enhance the platform.

Feel free to explore the sections that interest you, and don't hesitate to contribute to improving this documentation.


# Motivation & Challenges

TheOpenPresenter emerged from real-world challenges encountered in church multimedia ministry. This project aims to address various pain points identified through my years of hands-on experience.

### Church Multimedia Needs

Modern churches extensively use digital displays for various purposes, including:

* Presentation slides (Bible verses, teaching points)
* Song lyrics
* Announcements
* Video content

While many established solutions exist (Eg: EasyWorship, ProPresenter, OpenLP), each has its limitations. Most churches primarily need two core functions:

1. Slide presentation capabilities
2. Lyrics display system

However, church requirements often extend beyond these basics, with video playback being a common additional need.

## Challenges

Most existing software solutions excel at displaying song lyrics, which is typically the primary reason churches move beyond basic presentation tools like PowerPoint.

#### Integration Limitations

A significant gap exists in the connection between different media types, especially in more complex setups. Some solutions don't support video playback, while others can't handle presentation slides effectively, forcing users to switch between programs. While these tasks aren't inherently difficult, they add unnecessary complexity for volunteers, particularly in smaller churches where resources are limited.

#### Resource Requirement & Portability

These software solutions are typically resource-intensive, requiring at minimum a modern laptop for operation. Ideally, the software should run on any system, including mobile phones, for maximum flexibility - imagine needing to present but forgetting your laptop.&#x20;

Additionally, while control from mobile devices like phones and tablets is becoming standard in PA/Audio systems, it's not yet the norm for media presentation. Existing solutions often struggle with network requirements, making them unreliable.

#### Collaborative Features

Current software solutions are predominantly local in nature, making collaboration difficult when adding or changing content. Users frequently need to export and import files to share updates. The ideal solution would be local-first but enhanced with cloud synchronization to facilitate seamless collaboration.

#### Cost Barriers

Quality solutions come at a premium. For instance, ProPresenter, the industry standard, while worth its cost, is often not financially viable for smaller churches.

#### Limited Extensibility

Existing solutions are purpose-built and typically rigid in their functionality. Adding new capabilities or customizing features is rarely straightforward or even possible.

{% hint style="info" %}
While solutions exist for many of these individual challenges, there isn't currently a single solution that addresses all of them simultaneously.
{% endhint %}

### Shortcomings

While TheOpenPresenter aims to address these challenges comprehensively, we acknowledge this is an ambitious undertaking. Our development roadmap prioritizes these issues, though some solutions will be implemented in later phases.

#### Current Limitation

TheOpenPresenter is currently designed as an online-first solution, requiring a stable internet connection. While offline functionality is planned for future releases, it's important to note this current limitation.


# System Architecture

TheOpenPresenter is a web-based application built with a modular, multi-component architecture. The system comprises a robust backend server and specialized frontend applications, each serving distinct purposes. We also wrap the system as apps/desktop software.

## Core Components

### Backend Server

* Built with Node.js
* Implemented in TypeScript
* Almost everything will go through the server

{% hint style="info" %}
We will eventually package the server so that it can be run locally by users. This will enable local use without any internet connection.
{% endhint %}

### Frontend Applications

1. **Management Interface**
   * Built with React & Vite
   * Primary functions:
     * User account management
     * Project management
     * Landing page presentation
     * System navigation
2. **Remote Control Interface**
   * Built with React & Vite
   * Serves as a wrapper for plugin-based control functionality
   * Provides presentation control capabilities
   * Designed for use on mobile devices or tablets in additional to standard desktop use
3. **Renderer Interface**
   * Built with React & Vite
   * Handles content display and rendering
   * Acts as a wrapper for plugin-based rendering functionality
   * Designed for display screens/projectors
4. **Landing page**
   * Build with Astro
   * Handles everything in the landing page

Simply put, the **Remote** is used to control the presentations. And **Renderer** is used to show the presentation. Meanwhile, the Next.js management interface is used to navigate/open/create the correct Remote & Renderer. All of them communicates with the server to get the data it needs.

{% @mermaid/diagram content="graph TD
subgraph Admin Device
MI\[Management Interface<br/>Laptop/Desktop]
end

```
subgraph Control Device
    RC[Remote Control<br/>Phone/Tablet]
end

subgraph Display Screen
    RD[Renderer<br/>TV/Projector]
end

subgraph Backend Infrastructure
    NS[Node.js Server]
    DB[(Database)]
end

MI -->|"Opens"| RD
MI -->|"Opens"| RC
MI <-->|"Project/User Data"| NS

RC -->|"Sends control commands"| NS

RD <-->|"State updates"| NS

NS <-->|Data persistence| DB

style MI fill:#f9f,stroke:#333
style RC fill:#bbf,stroke:#333
style RD fill:#bfb,stroke:#333
style NS fill:#fb9,stroke:#333

classDef device fill:#f9f,stroke:#333,stroke-width:2px
classDef display fill:#bfb,stroke:#333,stroke-width:2px
classDef server fill:#fb9,stroke:#333,stroke-width:2px" %}
```

### Plugin System

TheOpenPresenter is powered by a plugin architecture.. All presentation features (e.g., video playback, slide shows) are implemented as plugins. This means that adding a functionality like playing videos is just a plugin away. Our core system provides various API that can be used to integrated the plugin to our system.

Here's the topology of a plugin:

* **Server-Side**
  * Written in Typescript
  * Runs on the backend
  * Integrates with core system APIs
* **Client-Side**:
  * Can include both remote control and renderer aspects
  * Supports any browser-compatible frontend technology
  * Integrates seamlessly with main interfaces


# Terminologies

### Remote

Remote is the frontend app that controls our Project state

### Renderer

Renderer is the frontend app that renders based state of our Project

### Project

This is the "document" that users work on. You can add presentations to it and you can also present from it. It powers our Remote and Renderer. The state itself is a Yjs document, stored in the database. More on this in the next few chapters.


# Server Communication

As previously described in [System Architecture](/introduction/system-architecture), TheOpenPresenter consists of three main frontend applications that users interact with.

### Next.js

The Next.js application communicates with the server primarily through GraphQL, which is powered by [Postgraphile](https://postgraphile.org/). This automatically generates the GraphQL schema directly from our PostgreSQL database. As a result, much of the business logic for user, project, and organization management resides within the database itself.

### Remote/Renderer

While both the Remote and Renderer applications use GraphQL endpoints to fetch certain data, they employ a separate mechanism for project synchronization using Yjs.&#x20;

Each plugin maintains its own state within a Yjs document. The Remote interface is used to modify this state, while the Renderer displays output based on the current state.

#### Reactivity

The Renderer operates on reactive programming principles - meaning its display updates automatically in response to changes in the plugin's state. If you're familiar with frameworks like React, this concept will be familiar: the UI is a direct reflection of the current state.

While this approach provides predictability and consistency, it also comes with typical reactive programming considerations. For example, implementing smooth animations and transitions requires careful planning since all changes are state-driven rather than directly manipulated.


# Project Topology

The main bulk of a project lives within a Yjs Document. The structure of the document can be seen in our Typescript type here: <https://github.com/Vija02/TheOpenPresenter/blob/main/packages/base-plugin/src/types.ts>

Here's how a project document might look like:

{% code overflow="wrap" %}

```json
{
  "meta": {
    "id": "project_01j96y7vz1f5c9pphfx0nkqg6h",
    "name": "Test Project",
    "createdAt": "2024-10-02T15:40:36.706Z"
  },
  "data": {
    "scene_01j9c3wn6mfcpafk2ee8jwcw5t": {
      "name": "Audio Recorder",
      "order": 1,
      "type": "scene",
      "children": {
        "plugin_01j9c3wn6mfcpafk2eecfj37s5": {
          "plugin": "audio-recorder",
          "order": 1,
          "pluginData": {
            "recordings": [],
            "activeStreams": []
          }
        }
      }
    },
    "scene_01j9740ysfemzsdyvqn2pyhb5m": {
      "name": "Radio",
      "order": 2,
      "type": "scene",
      "children": {
        "plugin_01j9740ysge6wv45h29rqkddtt": {
          "plugin": "radio",
          "order": 1,
          "pluginData": {
            "url": ""
          }
        }
      }
    }
  },
  "renderer": {
    "1": {
      "currentScene": "scene_01jbf18xagf24a30sf1wvf19wh",
      "overlay": null,
      "children": {
        "scene_01j9740ysfemzsdyvqn2pyhb5m": {
          "plugin_01j9740ysge6wv45h29rqkddtt": {
            "volume": 0.05,
            "isPlaying": false
          }
        },
        "scene_01j9c3wn6mfcpafk2ee8jwcw5t": {
          "plugin_01j9c3wn6mfcpafk2eecfj37s5": {}
        }
      }
    }
  }
}
```

{% endcode %}

{% hint style="warning" %}
Some parts of this document might still change. Particularly around `meta` and  `renderer`
{% endhint %}

## ID

All ID inside our document should be a [TypeID](https://github.com/jetify-com/typeid-js). Notice that they are also prefixed with strings like *scene*, *plugin* and *project*.

## Meta

The **meta** object stores any meta information about the project. It is quite basic at the moment but we may add more things in the future.

## Data

The majority of information is stored in the **data** object. A project can contain many scenes.

#### Scene

A scene controls what is shown in a single view. It contains one or more **plugins.**

#### Plugin

Each plugin is responsible for a specific functionality. Eg: Video playing. You can include multiple plugin inside a single scene.&#x20;

For example, you may have a plugin to show a moving background. And then use another plugin to show text on top of it.

Crucially, each plugin can store its own data in the document. This data will be used by the Renderer to render the right things.

## Renderer

#### Multiple Views

A project have many views at any single time. This is useful to show different views to multiple screens. For example, to be used as a confidence monitor.

This is the first level under renderer.

#### Active scene

Each view will show the scene that is set in the **currentScene** property.

Meanwhile, the **overlay** property is used to override/overlay the view with something else like a black screen.

#### Children

The children property maps the **scenes** and **plugins** that exists in **data**. Correspondingly, each plugin is responsible for the data that is stored inside those properties.

{% hint style="info" %}
While we have the concept of **views** and **active scenes**, a plugin does not have to strictly obey them. For example, an song player plugin probably wants to keep playing across scenes. However, we encourage to keep crossovers to the minimum.
{% endhint %}

## Custom state in data vs renderer

So there are 2 location where plugin can store their state. When should you store state in **data** vs **renderer**?

If your data will be the same across views, they should be stored in **data**.

If your data will be different across views, they should be stored in **renderer**.

**For example:**

A lyrics plugin may store its songs in the **data** property. However, it may choose to store the display information in the **renderer** property. This allows users to show different lyrics of the same song in screens within the same plugin.


# Quickstart

The best way to understand the project is to just try it!

The project is open-source and we are looking for contributors.

{% embed url="<https://github.com/Vija02/TheOpenPresenter>" %}

### Prerequisites

* Node LTS

Then, simply run these commands:

```bash
git clone git@github.com:Vija02/TheOpenPresenter.git

cd TheOpenPresenter

# Enable corepack & install yarn
# Note: You may need to run this as admin in Windows
corepack enable
corepack use yarn

# Setup deps & local DB
yarn && yarn local-dev

# On another terminal, start the server
yarn dev
```

You should then be able to access the project through <http://localhost:5678>

#### Future runs

For future runs, you only need to run `yarn local-dev` to run the DB, and `yarn dev`  to run the server itself.

`yarn dev` might take a long time to resolve since it's watching every single package in the repo. If you know which file you're modifying, you can watch only that package by going to its folder and running `yarn dev` there.

{% hint style="info" %}
We're constantly improving the development experience of TheOpenPresenter. If these steps doesn't work, please let us know!
{% endhint %}


# Static files

Some plugins may require us to serve many different media files. Rather than storing these in the main repo, they are stored in <https://github.com/Vija02/theopenpresenter-static/>

One way to access these files is directly through the github URL

However, to prevent CORS/ORB issue, it is recommended that you serve this file from a domain you control. There are many ways to do this like deploying a static server or proxying github.

Here is an example on how to handle this through a Cloudflare Worker:

```javascript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    
    // Construct the GitHub raw URL
    const githubUrl = url.pathname === '/' 
      ? 'https://raw.githubusercontent.com/Vija02/theopenpresenter-static/refs/heads/main'
      : `https://raw.githubusercontent.com/Vija02/theopenpresenter-static/refs/heads/main${url.pathname}`;

    // Fetch from GitHub
    const response = await fetch(githubUrl, {
      cf: {
        // Cache in Cloudflare's CDN for 1 year
        cacheTtl: 31536000,
        cacheEverything: true,
      }
    });
    
    // Clone the response and add CORS headers
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('Access-Control-Allow-Origin', '*');
    newResponse.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
    
    return newResponse;
  }
};
```

### Configuration

By default, we will directly use the github link to access the files. To override this, define a new path in the **STATIC\_FILES\_PATH** environment variable.


# Media Storage

We currently have 2 storage types that you can use.

1. File storage
2. S3 storage

This can be set through the `STORAGE_TYPE` environment variable. There are corresponding env that you can set to configure that storage type. S3 for example:

```
STORAGE_S3_BUCKET=
STORAGE_S3_REGION=
STORAGE_S3_ENDPOINT=
STORAGE_S3_ACCESS_KEY_ID=
STORAGE_S3_SECRET_ACCESS_KEY=
```

### Storage Proxy

The URL of the media will always be pointing to **your-url.com/media/data/\[file]**.

As such, you could serve the media through any methods by resolving that URL. It may be useful to hook it up to a CDN like Cloudflare to reduce unnecessary server load. In practice, you could set a redirect from that URL to another subdomain in Cloudflare to get granular access of what you serve.&#x20;

We also provide the `STORAGE_PROXY` env variable. This setting controls whether you want the server to serve the media on the path mentioned above.

When `STORAGE_PROXY` is **local**, it will serve the static files in the server. This only works if `STORAGE_TYPE` is **file**.

You could also set `STORAGE_PROXY` to any URL. This will in turn proxy the request to the specified URL.

Alternatively, pass any other value and the proxy will be disabled.

### Changing between storage type

We currently do not have any mechanism to handle changing between storage types. Changing how the server handles it is just a simple envvar change. However, you'll need to migrate the data between storage type manually.

### Serving media yourself

Cloudflare

Cache

CORS


# UI Component

Our UI stack of choice is [TailwindCSS](https://tailwindcss.com/) + [`shadcn`](https://ui.shadcn.com/) ([radix-ui](https://www.radix-ui.com/)). Our UI component lives in **packages/ui**. And while we use a lot of components from shadcn, it acts mainly as a base for us to work from.&#x20;

#### CSS usage

As much as possible, we should put our styles in a **.css** file. While tailwind allows for the style to be defined inline, we prefer this to be kept to a minimum. The main benefit of tailwind we are utilizing is the design system and its syntactic sugars.&#x20;

In general, we use the following convention for the css class name: `[repo-name]--[component-name]-[component-part]__[extra-info]` &#x20;

For example:

```
ui--alert-heading__warning
ui--alert-title
pl-lyrics--mobile-preview-pane__open
```

> &#x20;**pl** stands for plugin

### Micro-frontends and Tailwind

Tailwind doesn't work the greatest with micro-frontends. See <https://github.com/tailwindlabs/tailwindcss/discussions/6829> for more info.

In our app, we have several separation between components that leads to this issue. Eg: In remote, we have the container app, UI library, and also each plugin. So we need to somehow avoid the styles from clashing.

#### Solution

**UI Library -** On our UI library, the class names are mangled on build using [tailwindcss-mangle](https://github.com/sonofmagic/tailwindcss-mangle). This prevents the clashing with any other components

**Remote Container App -** As much as possible, all styles on the container apps should use custom css class names as recommended above

**Plugin -** Since the other components tries their best to avoid using the default tailwind class names, each plugin should be able to use default tailwind class names. However, we still recommend using custom ones like above, or a prefix


# Introduction

TheOpenPresenter leans heavily onto its plugin system. Once the core system is robust, most of the development should happen in plugins.&#x20;

In this section, we outline some of our official plugins along with explaining how plugins work & how to develop for them.


# Official Plugins

TheOpenPresenter provides a number of official plugins. Each plugin is responsible for handling a specific media that you want to show to the screen. For example, Slides, Lyrics, Videos, etc.

At the moment, we only have a small selection of plugins. This list will keep on growing and they will also evolve to be more generic over time.&#x20;


# Slides

Slides are our main display plugin used to pull in presentations from a variety of sources.

Right now we support Google Slides, PPTX and PDF.

Behind the scenes, our primitive is a PDF since they are as reliable as we can get. We convert google slides and PPTX into PDF. And then we generate images that is used to display the thumbnail. We can also use the images as the render.

For Google slides, we use its embed to mimic the functionality that exists there.

For PPTX, we currently only transform it into PDF and follow our basic image workflow. Eventually, we can very likely use powerpoint online to get this to render properly.

**Upcoming integration:**

* Canva

**Future exploration:**

[Collabora Online](https://collaboraonline.github.io/)


# Lyrics Presenter

At the moment, we only have integration with myworshiplist.com. This will increase in the future.&#x20;

We also want to add ability for users to save their own songs into the database. Currently, you can add a new song but it won't persist beyond the plugin.

We use a format inspired to OpenSong

Here are some of the basic rules:

1. Separate sections with square brackets(\[ ]) like \[Verse 1].\
   This can be anything from Verse, Chorus, Bridge, and any text you like.
2. Use a single dash(-) to split your section into multiple slides.
3. Add a dot(.) in front of a line to indicate that it is a chord line.\
   Note: At this time, we do not support showing chords yet.

#### Example

```
[Verse]
I know He rescued my soul
His blood has covered my sin
I believe I believe
-
My shame He's taken away
My pain is healed in His name
I believe, I believe
-
I'll raise a banner
'Cause my Lord
Has conquered the grave

[Chorus]
My Redeemer lives
My Redeemer lives
My Redeemer lives
My Redeemer lives

[Bridge]
You lift my burdens
I'll rise with You
I'm dancing on this mountaintop
To see Your kingdom come
```


# Lyrics compatibility

### Open Lyrics&#x20;

<https://docs.openlyrics.org/en/latest/dataformat.html>

Lib

<https://github.com/ChrisMBarr/OpenLyrics-parser>

<https://github.com/ChrisMBarr/LyricConverter?tab=readme-ov-file#creating-a-new-input-type>

### Song Database

<https://github.com/Jesulayomy/OpenLP/tree/master/Songs>

<https://worshipleaderapp.com/en/download-song-database-opensong-openlp-and-quelea>

### Different formats

<https://manual.openlp.org/songs.html#song-importer>

OpenLP have plenty of docs in their wiki

<https://gitlab.com/openlp/wiki>

<https://gitlab.com/openlp/wiki/-/wikis/Development/EasySlides_-_Song_Data_Format>

### Misc

<https://opensong.org/development/file-formats/>


# Video Player

Known issue:

* In old android Webview, player buffers indefinitely
  * Reloading the component fixes this for some reason


# Audio Recorder

Very unreliable at the moment. It seemingly needs a reliable internet connection to work.

We need to handle errors better and store locally for backup.


# Developing a Plugin


# Plugin API


# Yjs


# Awareness


# Scene


# Renderer

### PluginRendererState

\_\_audioIsPlaying

\_\_audioIsRecording


# Backend


# Yjs Handler


# Loading Frontend


# Security


# Frontend

#### Custom view <a href="#custom-view" id="custom-view"></a>

TheOpenPresenter uses Web Components as the mechanism to load views from its plugins. Plugin creators can use web component directly or use a wrapper if they are using a framework like React or Vue.


# Remote


# Renderer


# Media


# Server Plugin API

serverPluginApi.uploadMedia


# Frontend

### Headers

To get upload working, we need a few information:

**csrf-token**

<pre class="language-typescript"><code class="lang-typescript"><strong>{ "csrf-token": pluginApi.env.getCSRFToken() }
</strong></code></pre>

**organization-id**

<pre class="language-typescript"><code class="lang-typescript"><strong>{ "organization-id": pluginApi.pluginContext.organizationId }
</strong></code></pre>

**file-extension (optional)**

If none is passed, we will read from the original file name. If none, then it'll be empty.&#x20;

For tus, the original file name is obtained from `metadata.filename`&#x20;

For form data, the original file name is obtained from the `filename` field.

**custom-media-id (optional)**

The resulting id of the media. If none is passed, we will generate one.


# Tus

Tus-js-client

Uppy

### **Endpoint**

The tus endpoint can be obtained through the pluginApi:

```typescript
pluginApi.media.tusUploadUrl
```

### Headers

We do not require any additional headers other than the one specified at [Frontend](/plugins/developing-a-plugin/plugin-api/media/frontend#headers). However, all Tus headers should be passed as normal.&#x20;

### Metadata

At the moment of writing, we only read the metadata for the file's original name and to infer the file extension based on that.&#x20;

We recommend to pass the same metadata as it would appear when you use Uppy since that is what we use. Here's an example of one:

```javascript
{
  relativePath: 'null',
  name: 'title.png',
  type: 'image/png',
  filetype: 'image/png',
  filename: 'title.png'
}
```


# Form Data

{% hint style="info" %}
Whenever possible, we recommend using the [Tus](/plugins/developing-a-plugin/plugin-api/media/frontend/tus) endpoint instead.
{% endhint %}

### **Endpoint**

The form-data endpoint can be obtained through the pluginApi:

```typescript
pluginApi.media.formDataUploadUrl
```

### Headers

In addition to the headers described in [Frontend](/plugins/developing-a-plugin/plugin-api/media/frontend#required-headers), we also require the following headers to be set:

**upload-length**

The size of the uploaded media/file.

Here's an example of including these data using Uppy:

```typescript
new Uppy().use(XHR, {
  endpoint: pluginApi.media.formDataUploadUrl,
  headers: (file) => ({
    "csrf-token": appData.getCSRFToken(),
    "organization-id": pluginApi.pluginContext.organizationId,
    ...(file.size
      ? {
          "upload-length": file.size.toString(),
        }
      : {}),
  }),
})
```


# TRPC


# Database


# Audio

### Playing Audio

Browsers have a limitation where you can't play audio until the user interacts with the tab. An attempt to play audio anyway will result in an error thrown.

To work with this limitation, we provide a React hook that will tell you whether the renderer can play audio or not. If playing video, you could use this flag to indicate when to unmute the video.

Example usage:

```typescript
const canPlay = pluginApi.audio.useCanPlay({ skipCheck: !isPlaying });
```

Calling this function by default will:

1. Periodically check the renderer for a status change
2. Show a warning to the user that audio is blocked

We recommend passing a **skipCheck** property that will only start performing these side effects when the value is false. See the example usage above for clarity.

### Audio Volume

If making a volume fader, please keep in mind that by default, HTML audio scales linearly. This is not ideal since human perception of loudness is not linear.

See more: <https://www.dr-lex.be/info-stuff/volumecontrols.html>

Example library to use: <https://github.com/discord/perceptual>

### Showing to user that Audio is playing

If your plugin plays audio, you'll want to indicate that to the user to reduce confusion.

For example, we display an icon next to the scene so that the user knows where the audio comes from. Browser tabs use the same UI to notify this to the user.

Here's how it looks like in our platform:

<div align="left"><figure><img src="/files/bXfeMmFI8g7d2WSQXeeY" alt=""><figcaption></figcaption></figure></div>

To set this state, you need to set the `__audioIsPlaying` property in your renderer data to either true or false. See the Typescript type **PluginRendererState** for all possible states.&#x20;

{% embed url="<https://github.com/Vija02/TheOpenPresenter/blob/main/packages/base-plugin/src/types.ts>" %}

**Example**

```typescript
const onRendererDataLoaded: RegisterOnRendererDataLoaded<PluginRendererData> = (
  rendererData,
) => {
  const yjsWatcher = new YjsWatcher(rendererData as Y.Map<any>);
  yjsWatcher.watchYjs(
    (x: PluginRendererData) => x.isPlaying,
    () => {
      // Watch the isPlaying property and sync it to __audioIsPlaying
      rendererData.set("__audioIsPlaying", rendererData.get("isPlaying"));
    },
  );

  return {
    dispose: () => {
      yjsWatcher.dispose();
    },
  };
};
```

More info about this here: [Renderer](/plugins/developing-a-plugin/plugin-api/yjs/renderer)

### Showing to user that Audio is being recorded

Similarly to above, you can show to the user that audio is currently being recorded by setting the `__audioIsRecording` variable.


# Viewer State

When a viewer loads a plugin, it might take some time for the data to be loaded.

While the data is loading, we should show an indication to the user. Here's how it looks like on the sidebar:

<figure><img src="/files/iBmOHhGHRXVhr2hPW0M7" alt=""><figcaption></figcaption></figure>

How to:

```typescript
pluginApi.awareness.setAwarenessStateData({ isLoading: true });
```


# Notifying Errors

We provide a functionality to report errors/warnings to the user.

1. TODO: Register an error code
2. Use Plugin API to add/remove the error


# Caveats

Sharing files between plugins


# Sharing dependencies

Some dependencies are shared among all frontend components. We do this for 2 main reasons:

1. So that the client doesn't have to load the dependencies multiple times
2. Some library requires that only one copy of itself is used. Eg: Yjs

We achieve this using [`importmap`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), provided by our `Remote` and `Renderer` wrapper.

This means that for frontend plugin bundles, you should exclude these dependencies from the bundle. Instead, it should import the dependency directly using a bare specifier.

You can look at the build config of any of the plugins in the main repository. They serve as an example of how the build pipeline should be setup.

### Shared dependencies

At the moment of writing, here are the list of shared dependencies:

* `yjs`
* `react` (react, react-dom, react-dom/client)

To see an up-to-date list, refer to the `importmap` definition in the main repo:

{% embed url="<https://github.com/Vija02/TheOpenPresenter/blob/main/apps/remote/index.html>" %}


# Cross-over between plugins

In a single page, there may be multiple copies of the same plugin. All these plugins share the same code. This means that it is possible for different copies of a plugin to interact with each other within a project.

A manifestation of this is if your plugin uses a global object. All the plugin importing from this file will hold the same reference which is likely not desirable.

To avoid this, simply make sure your code is encapsulated well. For example, by wrapping stores in classes and locating them in each plugin copy rather than the file.


# Performance

Keeping the renderer quick is a big priority. It may be run in many different situation with varying network & CPU capability.

For this reason, the renderer should pull as little dependency as possible to keep the size low

And it should also do the minimum required to keep the CPU/GPU requirement low

Lazy loading components is critical and splitting the render & remote bundle is highly recommended


# Introduction

One of our main goal is to make the software available everywhere. With cloud, this is simple. But in the presentation world, offline is king.

This section explains how we made TOP accessible even when focused on its offline & local first capabilities.

### Cloud

An organization can be connected to the "cloud". A cloud is any TOP instance that is generally accessible. When connected, you will be able to access its projects through your local instance.&#x20;

#### Host Projects

Additionally, the cloud instance will also be able to access your local instance. This is only available when your local instance is running. While you do not have to use cloud to access your local projects, the cloud offers an easy and reliable way to establish connection to your local projects.&#x20;

### Local discovery

This is yet to be implemented


# Host Projects

Host projects are a feature in TOP that allows users to access projects in local instances through an existing cloud connection.&#x20;

> To put it into picture:&#x20;
>
> * Desktop app connects to cloud & opens a project locally
> * A separate device connects to <https://theopenpresenter.com>
>   * And accesses the local project
>   * All request will be proxied through the cloud

#### How does this work?

When a desktop instance with a cloud connection is online, it will:&#x20;

1. Ping the cloud instance to notify of its existence
   1. This includes the details required for us to connect to the instance (iroh ticket)
   2. As well as the currently active projects
2. In the cloud, we show the open projects for all host projects
3. Opening it will open a project URL with specific queries
   1. We then use this queries to know how to proxy the request
      1. `pOrg` and `pEndpoint` in the URL
      2. Backend wise, we use `x-organization-slug` and `x-iroh-endpoint-id` as headers to identify that this should be proxied.
   2. An iroh endpoint is opened on the cloud, which is then used to proxy the request

> Note: When accessing a host project, the organization and project slug in the URl should be of the hosts. This is because our code reads the URL to determine what should be fetched. On the cloud, we read the search query to know how to proxy.&#x20;
>
> So this can be confusing, but practically, it looks something like this:
>
> https\://**\[cloud]**.com/**\[localRoute]**?**\[cloudProxyDetails]**


# Plugin Context


# Playing Audio

Autoplay behaviour


# Listen to scene changes

`onSceneVisibilityChange`


# Ambition

**Server as a local app**

Alternatively, the server is also available as a local app. This is the preferred method for locations with unreliable or no internet connection.

In this configuration, you can download and install an app that will handle everything, just like how other presentation software is designed.

\
**3a. Browser rendering**

Browser rendering is the default rendering method. It is the simplest and the recommended way for anything simple.

**Media cache**

Since rendering is done in this part, we use media cache to ensure smooth media playback and transition. If there are multiple renderer connected, each of them will need a cache of their own.

**3b. Server rendering**

Server rendering renders the final picture in the server. This is required if we want an output in a specific format.\
For example: Video stream like NDI/SDI/HDMI/RTSP and the like.

The benefit is that there won't need to be a client. Or at least it can be a very simple client.


# Background music

Lofi

<https://open.spotify.com/playlist/3iWqG37X1QamNZhNuP9NdR?si=A1VVCWhtQoea5eoCed-UhA>

Various playlist

<https://open.spotify.com/user/freeccm/playlists>

Jazz Cafe

<https://www.youtube.com/watch?v=D3rOJYZjQHE>


# Environment Variables

The server can be configured by passing in different environment variables. Here are some of them:

#### DISABLE\_HSTS

Enabling this flag will disable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) from [helmet](https://github.com/helmetjs/helmet). This is useful for local deployment and our desktop build so that users doesn't have to use HTTPS to access the app.

#### AUTO\_LOGIN

Setting this to **"1"** will automatically create an anonymous user and use that to authenticate every client. This is useful in deployments where the concept of user is redundant like our desktop build.

#### ALLOW\_ANY\_ORIGIN

When enabled, we will relax security when being accessed from a random origin. Useful for local deployment like our desktop build.

#### UPLOADS\_PATH

When using the `file` storage, you can use this property to set the folder where the files will be saved. Otherwise, it will default to an `uploads` folder in the root directory.

#### PLUGINS\_PATH

The folder where plugins will be loaded from. The default is to a `loadedPlugins` folder in the root directory.


# Package dependencies

The repo contains many packages, separated by what it's supposed to do. Here's a very rough graph of how everything is connected together.

Please keep in mind that the `server` also serves everything built for the frontend. `homepage`, `remote` and `renderer` are hot-reloaded. Anything in the plugin are not.

Also, while `db` is not directly connected in the chart, in reality, `graphql` generates its schema directly from the database

```mermaid
graph TD
    %% Foundation
    config-presets["config-presets<br/>(ts/eslint/prettier/tailwind)"]
    graphql[graphql]
    base-types[base-types]
    lib[lib]
    observability[observability]
    ui[ui]
    backend-shared[backend-shared]
    base-plugin[base-plugin]
    video[video]
    media-picker[media-picker]
    test[test]

    %% Foundation edges
    graphql --> config-presets
    base-types --> config-presets
    lib --> graphql
    observability --> lib
    ui --> graphql & lib
    backend-shared --> lib & observability
    base-plugin --> backend-shared & base-types & graphql & lib & observability & ui
    video --> base-plugin & base-types & lib
    media-picker --> base-plugin & graphql & lib & ui & video
    test --> base-plugin

    %% Plugins
    subgraph Plugins
        p-audio[plugin-audio-recorder]
        p-embed[plugin-embed]
        p-lyrics[plugin-lyrics-presenter]
        p-radio[plugin-radio]
        p-image[plugin-simple-image]
        p-slides[plugin-slides]
        p-timer[plugin-timer]
        p-video[plugin-video-player]
        p-pads[plugin-worship-pads]
    end
    p-audio --> base-plugin & observability & ui & test
    p-embed --> base-plugin & ui
    p-lyrics --> base-plugin & observability & ui & video
    p-radio --> base-plugin & ui
    p-image --> base-plugin & lib & observability & ui
    p-slides --> base-plugin & lib & observability & ui
    p-timer --> base-plugin & ui
    p-video --> base-plugin & lib & observability & ui & video
    p-pads --> base-plugin & lib & ui

    %% Frontend apps
    subgraph Apps
        homepage[homepage]
        project[project]
        remote[remote]
        renderer[renderer]
        shared[shared]
    end
    shared --> graphql & base-plugin & lib & ui
    project --> base-plugin & media-picker & observability & config & graphql & lib & ui
    remote --> observability & base-plugin & graphql & lib & media-picker & p-lyrics & shared & ui
    renderer --> graphql & observability & base-plugin & lib & p-lyrics & shared & ui

    %% Backend
    subgraph Backend
        config[config]
        db[db]
        server[server]
        worker[worker]
    end
    config --> config-presets
    server --> backend-shared & base-plugin & config & graphql & lib & observability & portable-file
    worker --> backend-shared & base-plugin & config & lib & observability

    %% Misc / leaf
    e2e --> graphql
    portable-file --> config-presets
    embedded-postgres --> config-presets

    %% No internal deps: db, homepage(only config), desktop-screen,
    %% native-screen, vega-screen, tauri, base-types, prettier-config
```


# FAQ

<details>

<summary>Will you have a tvOS build? (Apple TV)</summary>

Apple TV doesn't allow us to render a website within an app. This is problematic because we rely on the web to render the presentation. Due to this technical limitation, we don't have any plans of supporting this at this moment.&#x20;

The solution to get this working is for us to create a separate native renderer for each plugin and functionality. While not impossible, this is a lot of extra work just to support Apple TV.

</details>


