# Access meeting audio with the RTMS SDK > This quickstart is for meetings, but can easily be repurposed for webinars. Just replace the meeting scopes and events with their webinar equivalent. For example, replace `meeting.rtms_started` with `webinar.rtms_started`. For this quickstart, we'll use the Realtime Media Streams (RTMS) [SDK](https://github.com/zoom/rtms) to print meeting audio to our terminal. Our app will join a meeting with a user using the auto-start feature. > **The RTMS SDK is helpful, but not required!** > > To create your own WebSockets follow the quickstart to [Get transcripts using WebSockets](/docs/rtms/meetings/quickstart-websockets/). Follow along with this video to complete the quickstart. [![Realtime Media Streams Quickstart with RTMS SDK for NodeJS](https://img.youtube.com/vi/Ag11LGA6H9I/maxresdefault.jpg)](https://www.youtube.com/watch?v=Ag11LGA6H9I) In this quickstart we will: 1. Create a server on a local tunnel 2. Install the RTMS SDK 3. Enable RTMS features on a Zoom app 4. Access real-time meeting audio If you're looking for a more in-depth guide to connect to RTMS streams, see [sample apps & recipes](/docs/rtms/sample-apps). ## Step 1: Clone the quickstart app and install the RTMS SDK To get started, we need to clone the repository from GitHub and install the RTMS SDK. 1. Run the command to clone the [RTMS quickstart app](https://github.com/zoom/rtms-quickstart-js) NodeJS app. ```shell git clone https://github.com/zoom/rtms-quickstart-js.git ``` 2. Run the command to navigate into the project and install dependencies. This installs the [RTMS SDK](https://github.com/zoom/rtms). For more information, see the [SDK Reference](https://zoom.github.io/rtms/js/). ```shell cd rtms-quickstart-js npm install ``` 3. Run the command to clone the [RTMS quickstart app](https://github.com/zoom/rtms-quickstart-py) Python app. ```shell git clone https://github.com/zoom/rtms-quickstart-py.git ``` 4. Run the command to navigate into the project, create a virtual environment, and install dependencies. This installs the [RTMS SDK](https://github.com/zoom/rtms). For more information, see the [SDK Reference](https://zoom.github.io/rtms/py/). ```shell # Create virtual environment python3 -m venv .venv # Activate virtual environment source .venv/bin/activate # On Windows: .venv\Scripts\activate # Install dependencies pip install python-dotenv # Install the SDK pip install rtms ``` Run the command to copy the example environment file. We'll add credentials to this file in **Step 4**. ```shell cp .env.example .env ``` ## Step 2: Create an RTMS client on a local tunnel Open the project in your code editor. The following code uses the [RTMS SDK](https://github.com/zoom/rtms) to: 1. Create a webhook event handler for `meeting.rtms_started` 2. Create a client to connect to an RTMS server using the webhook event's payload 3. Show an example of setting media params 4. Handle audio data 5. Log data to the console ### To create an RTMS client on a local tunnel 1. Replace any code in your `index.js` file. ```javascript // Import the RTMS SDK import rtms from "@zoom/rtms"; let clients = new Map(); // Set up webhook event handler to receive RTMS events from Zoom rtms.onWebhookEvent(({ event, payload }) => { const streamId = payload?.rtms_stream_id; if (event == "meeting.rtms_stopped") { if (!streamId) { console.log( `Received meeting.rtms_stopped event without stream ID`, ); return; } const client = clients.get(streamId); if (!client) { console.log( `Received meeting.rtms_stopped event for unknown stream ID: ${streamId}`, ); return; } client.leave(); clients.delete(streamId); return; } else if (event !== "meeting.rtms_started") { console.log(`Ignoring unknown event`); return; } // Create a new RTMS client for the stream if it doesn't exist const client = new rtms.Client(); clients.set(streamId, client); client.onAudioData((data, size, timestamp, metadata) => { console.log( `Received ${size} bytes of audio data at ${timestamp} from ${metadata.userName}`, ); }); // Join the meeting using the webhook payload directly client.join(payload); }); ``` 1. Replace any code in your `main.py` file. ```python import rtms import threading from dotenv import load_dotenv load_dotenv() # Store active clients by stream ID clients = {} # Queue for pending client setups (webhook thread -> main thread) pending_setups = [] pending_lock = threading.Lock() @rtms.onWebhookEvent def handle_webhook(webhook): event, payload = webhook.get('event'), webhook.get('payload', {}) stream_id = payload.get('rtms_stream_id') if event == 'meeting.rtms_stopped': if not stream_id: print('Received meeting.rtms_stopped event without stream ID') return client = clients.get(stream_id) if not client: print(f'Received meeting.rtms_stopped event for unknown stream ID: {stream_id}') return client.leave() del clients[stream_id] return if event != 'meeting.rtms_started': print('Ignoring unknown event') return # Queue this for processing on main thread (client creation must happen there) with pending_lock: pending_setups.append(payload) def process_pending_setups(): """Process pending client setups - must be called from main thread""" with pending_lock: if not pending_setups: return setups = pending_setups[:] pending_setups.clear() for payload in setups: stream_id = payload.get('rtms_stream_id') # Create client on main thread so _main_thread_id is correct client = rtms.Client() clients[stream_id] = client @client.onAudioData def on_audio(data, size, timestamp, metadata): print(f"Received {size} bytes of audio data at {timestamp} from {metadata.user_name}") # Join (now _process_join_queue isn't needed since we're on main thread) client.join( meeting_uuid=payload.get('meeting_uuid'), rtms_stream_id=payload.get('rtms_stream_id'), server_urls=payload.get('server_urls'), signature=payload.get('signature') ) if __name__ == '__main__': import time try: while True: process_pending_setups() for client in list(clients.values()): client._poll_if_needed() time.sleep(0.01) except KeyboardInterrupt: for client in clients.values(): client.leave() ``` 1. Expose this app on a local server over HTTPS. For this quickstart, we'll use [ngrok](https://ngrok.com/downloads/). You can also use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), [localtunnel](https://www.npmjs.com/package/localtunnel), or [other alternatives](https://github.com/anderspitman/awesome-tunneling). 1. [Install ngrok and setup your account](https://ngrok.com/docs/getting-started/). 2. Open a new terminal window and run the command to open an HTTPS forwarding URL at port `8080`. This outputs a public URL we'll use to receive [`meeting.rtms_started`](/docs/rtms/notifications/#meetingrtms_started) webhook events. ```shell ngrok http 8080 ``` ## Step 3: Set up a Zoom app to use RTMS The next step is to set up an RTM-enabled app with the `meeting:read:meeting_audio` scope. For more information, see [Add Realtime Media Streams to your app ](/docs/rtms/meetings/add-features/). Now that you've created the app, we need to add the ngrok URL from the previous step to your app and add your app to your account so we can test our implementation. ### To add your ngrok URL and add your app 1. Sign into the [Zoom App Marketplace](https://marketplace.zoom.us/). 2. To go to the app, in the upper-right of the screen, choose **Manage**. 3. Select your app from the list. 4. In the navigation pane, choose **Basic Information**. 5. In the **OAuth Information** section, for **OAuth Redirect URL**, add your ngrok URL. 6. In the navigation pane, choose **Local Test**. 7. In the **Add app** section, choose **Add app now**. 8. On the page that appears, choose **Allow**. The page will then redirect to a page that doesn't exist because we didn't set up OAuth on our ngrok URL. Now that the app is installed it's time to add the credentials to your `.env` file. ## Step 4: Add credentials to .env file Now that your app is created, we need to add the credentials to our `.env` file so the server can communicate with the app. ### To add the credentials 1. Sign into the [Zoom App Marketplace](https://marketplace.zoom.us/). 2. To go to the app, in the upper-right of the screen, choose **Manage**. 3. Select your app from the list. 4. Copy the **Client ID** and **Client Secret**. 5. Open the `.env` file in your project and paste the client ID and secret. - `ZM_RTMS_CLIENT` = the client ID from your app. - `ZM_RTMS_SECRET` = the client secret from your app. 6. Save the file. Now you're ready to test the server and app combination. ## Step 5: Start the app and join a meeting Now that your app is installed, your account may need to give permission to apps to access meeting content. Navigate to your [account settings page](https://www.zoom.us/account/setting) (as an account admin) and enable apps to [share realtime meeting content with apps](/docs/rtms/meetings/ux-host-admin-tools-ctrls/#admin-web-portal-controls). For this quickstart, we want to show how a user can auto-start an RTMS session whenever they join a meeting. Because you've installed the app for your user, you'll now be able to set the app to auto-start in your Zoom settings. ### To set RTMS to auto-start 1. Open your [Zoom settings](https://zoom.us/profile/setting). 2. Choose the **Zoom Apps** tab. 3. In the **Auto-start apps that access shared realtime meeting content** section, choose **+ Choose an app to auto-start**. 4. From the dropdown, select your RTMS app, and choose **Save**. ### To test your app 1. Run the command to set up the server to listen to incoming webhook events anytime the app is launched in a meeting. ```shell npm run start ``` ```shell python main.py ``` 2. Log into the Zoom desktop client as the user who has installed the app. 3. Launch a Zoom Meeting. Realtime Media Streams will launch as the meeting begins. The RTMS SDK will now log audio RTMS events and audio data to the terminal: ```shell webhook | 2025-07-08T05:42:53.395Z | DEBUG | Received webhook request: / webhook | 2025-07-08T05:42:53.395Z | INFO | Received event: meeting.rtms_started client | 2025-07-08T05:42:53.395Z | DEBUG | Setting video parameters: {"contentType":3,"codec":7,"resolution":1,"dataOpt":3,"fps":30} client | 2025-07-08T05:42:53.395Z | DEBUG | video parameters set successfully client | 2025-07-08T05:42:53.395Z | DEBUG | Setting deskshare parameters: {"contentType":3,"codec":7,"resolution":1,"dataOpt":3,"fps":30} client | 2025-07-08T05:42:53.396Z | DEBUG | deskshare parameters set successfully rtms | 2025-07-08T05:42:53.396Z | DEBUG | SDK already initialized client | 2025-07-08T05:42:53.396Z | INFO | Joining meeting: 12rg1/3FSzihG3R0yOjb4A== rtms | 2025-07-08T05:42:53.396Z | DEBUG | Generating signature for client: aaabbbcccdddeeefffgggx, uuid: 12rg1/3FSzihG3R0yOjb4A==, streamId: 123abcdefg5a4317ae22924fc1c91669 client | 2025-07-08T05:42:53.397Z | INFO | Successfully joined meeting: 12rg1/3FSzihG3R0yOjb4A== client | 2025-07-08T05:42:53.397Z | DEBUG | Starting polling with interval: 0ms Received 960 bytes of audio data at 1751953373930 from Dev Eloper Received 640 bytes of audio data at 1751953373952 from Dev Eloper Received 640 bytes of audio data at 1751953373973 from Dev Eloper Received 640 bytes of audio data at 1751953373994 from Dev Eloper Received 640 bytes of audio data at 1751953374015 from Dev Eloper Received 640 bytes of audio data at 1751953374036 from Dev Eloper Received 640 bytes of audio data at 1751953374057 from Dev Eloper webhook | 2025-07-08T05:43:00.432Z | DEBUG | Received webhook request: / webhook | 2025-07-08T05:43:00.432Z | INFO | Received event: meeting.rtms_stopped client | 2025-07-08T05:43:00.432Z | INFO | Leaving meeting: 12rg1/3FSzihG3R0yOjb4A== client | 2025-07-08T05:43:00.432Z | DEBUG | Stopping polling client | 2025-07-08T05:43:00.432Z | INFO | Successfully left meeting ```