A WebRTC data channel carries bidirectional text and binary messages over the same peer connection as the audio and video, with no separate socket or signaling path. Wowza Streaming Engine (WSE) can relay those messages between a publisher and its viewers, or pass them to a custom Java module for application-specific routing. Common uses are live captions, chat, control messages, and in-band metadata alongside the stream.
Data channels are only available in WSE 4.12+ with the current WebRTC implementation. They aren't supported on the legacy implementation, which remains available for other workflows.
About data channels
Data channel messages travel over SCTP on top of the same DTLS transport that secures the audio and video. This means they are encrypted and don't need extra ports or firewall rules. The side that opens a channel gives it a unique label and chooses the channel's reliability and ordering. WSE uses the label, for example chat or captions, to route the channel.
Data channels are scoped to a single app and stream name: two streams in the same app will never see each other's messages. Simulcast renditions are the single exception: a viewer playing a simulcast rendition is scoped to the rendition's source stream, so viewers of myStream and myStream_360p share the publisher's channels.
Data channels are disabled by default and must be enabled per application.
Notes:
- Each session must negotiate audio or video. If media is not negotiated, WSE rejects the whole session.
- The SCTP port (
5000) and the advertised maximum message size (65,535 bytes) are not configurable.
Broadcast data channel messages
By default, WSE broadcasts messages from the publisher are broadcast to each stream viewer The publisher's client opens a channel, and WSE opens a channel with the same label toward every viewer of that stream. A viewer that joins later gets a channel for every label the publisher already has open. When the publisher closes a label, WSE closes the matching channel on every viewer.
Prerequisites
- WSE 4.12+ with the current WebRTC implementation
- A WebRTC-enabled live app
- A client that opens data channels, for example the Wowza WebRTC example pages.
1. Enable data channels
Enable data channels in WSE Manager
- Open WSE Manager.
- Click the Applications tab.
- Select your WebRTC-enabled live app.
- Click WebRTC.
- Click Edit.
- Under Data Channels, select Enable Data Channels.
- Click Save.
- Restart the app to apply the changes.
Enable data channels in Application.xml
- Open your app's
[install-dir]/conf/[app-name]/Application.xmlconfiguration file. - Navigate to the
<WebRTC>→<Properties>container. - Add the
webrtcEnableDataChannelsproperty and set its value totrue. (See the code snippet below.) - Restart the app to apply the changes.
<!-- Example WebRTC data channels property --><!-- The property below belongs in the <WebRTC>/<Properties> container of Application.xml --><Property> <Name>webrtcEnableDataChannels</Name> <Value>true</Value> <Type>Boolean</Type> </Property>
2. Customize message routing (optional)
By default, WSE broadcasts messages from the publisher are broadcast to each stream viewer.
If broadcasting messages doesn't suit your use case, you may configure your app to use a custom module to route data channel messages. (See the module routing section below for more information.)
3. Test the workflow
- Open the WebRTC publish test page.
- Select Enable Chat, and publish. The data channel panel should reach open within a couple of seconds, before any viewer connects.
- In a second tab, open the WebRTC play page for the same stream with chat enabled and wait for its panel to show open.
- Send a message from each side and confirm it arrives on the other.
- Reconnect both sides with Enable Captions selected and confirm the caption lines appear as an overlay on the play page.
- To confirm the routing scope, open a second viewer: a message from the publisher reaches both viewers, and a message from one viewer reaches only the publisher.
Route data channel messages with a custom module
When broadcasting data channel messages to all viewers doesn't suit your needs, you can configure your app to route messages with a custom Java module. This adds flexibility and allows for additional use cases such as chat rooms, viewer-to-viewer messages, or a server-authoritative control channel.
In module mode, WSE applies no routing of its own. Each channel stays point-to-point with the client on its peer connection, so any fan-out is your module's job. Channel events reach your module as module special functions:
onWebRTCDataChannelSessionCreate: The peer connection's SCTP association is up. Your module can open channels toward the client.onWebRTCDataChannelOpen: A channel is open and usable.onWebRTCDataChannelStringMessageandonWebRTCDataChannelBinaryMessage: One whole message has arrived.onWebRTCDataChannelClose: The channel closed.onWebRTCDataChannelSessionDestroy: The connection is gone. It fires once, after every open channel has reported closed.
The IModuleOnWebRTCDataChannel interface documents these names and call signatures. Your module implements the methods themselves, not the interface.
Each callback hands your module a session or a channel:
- An
IWebRTCDataChannelSessionis one peer connection, or one client. It exposes the application instance, stream name, connection ID, and whether the connection is the publisher. Your module selects on these values to build its own routing. - An
IWebRTCDataChannelis one channel on one session, so a send on it reaches only that session's client. Sends are non-blocking and thread-safe.
For a single peer connection, callbacks are serialized and ordered. Session create comes first. Then, for each channel, open, its messages in arrival order, and close. Session destroy comes last, exactly once. Different peer connections dispatch concurrently, so any state your module shares across connections must be thread-safe.
Module callbacks run on a shared thread pool, so they must return promptly; hand slow work to your own executor. To limit the number of message callbacks that may queue per connection, update the app's webrtcDataChannelMaxPendingDispatchMessages configuration property (512 by default). Messages queued beyond the limit are dropped with a warning.
Enable module routing
Follow the steps below to use a custom module to route your data channel messages.
- Enable data channels for the app, as described in Enable data channels above.
- Open the app's
[install-dir]/conf/[app-name]/Application.xmlconfiguration file. - Navigate to the
<WebRTC>→<Properties>container. - Set the value of the
webrtcDataChannelModeproperty toMODULE.<!-- Example WebRTC data channel mode property --><!-- The property below belongs in the <WebRTC>/<Properties> container of Application.xml --><!-- May be set to BROADCAST or MODULE (case insensitive) --><Property> <Name>webrtcDataChannelMode</Name> <Value>MODULE</Value> <Type>String</Type> </Property> - Build your module into a JAR and copy it to
[install-dir]/lib. See Use Wowza Streaming Engine Java modules for more information. - Add the module to the
<Modules>container ofApplication.xml.<!-- The entry below belongs in the <Modules> container of Application.xml --><Module> <Name>ModuleWebRTCDataChannelChatRoom</Name> <Description>Routes WebRTC data channel messages as a per-stream chat room</Description> <Class>com.mycompany.wms.ModuleWebRTCDataChannelChatRoom</Class> </Module> - Restart the app to apply the changes.
Example module
The module below turns each stream into a chat room: a message from any participant reaches every other participant of the same stream, viewers included.
The publisher opens a chat channel, and the module opens one toward each viewer. The module also opens a notice channel toward each client, which carries server-authored messages only. A client never has to open it.
package com.mycompany.wms;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import com.wowza.wms.module.ModuleBase;
import com.wowza.wms.webrtc.datachannel.IWebRTCDataChannel;
import com.wowza.wms.webrtc.datachannel.IWebRTCDataChannelSession;
/** Routes WebRTC data channel messages as a per-stream chat room */
public class ModuleWebRTCDataChannelChatRoom extends ModuleBase
{
/** Opened by the publisher, and by WSE toward each viewer. Carries the chat messages this module fans out. */
private static final String CHAT_LABEL = "chat";
/** Opened by WSE toward each client. Carries server-authored notices only. */
private static final String NOTICE_LABEL = "notice";
/** The participants of each stream. */
private final Map<String, Set<IWebRTCDataChannelSession>> rooms = new ConcurrentHashMap<>();
/** The peer connection's SCTP association is up. */
public void onWebRTCDataChannelSessionCreate(IWebRTCDataChannelSession session)
{
rooms.compute(session.getStreamName(), (streamName, room) -> {
if (room == null)
room = ConcurrentHashMap.newKeySet();
room.add(session);
return room;
});
// Asynchronous: the channel arrives back through onWebRTCDataChannelOpen once it is ready.
session.openChannel(NOTICE_LABEL);
// Viewers of the example play page never open a chat channel themselves; they wait for WSE to
// open one toward them, so this module does it. The publisher already opens its own.
if (!session.isPublisher())
session.openChannel(CHAT_LABEL);
getLogger().info("ModuleWebRTCDataChannelChatRoom: " + describe(session) + " joined");
}
/** A channel is usable, whether the client opened it or this module did. */
public void onWebRTCDataChannelOpen(IWebRTCDataChannel channel)
{
if (NOTICE_LABEL.equals(channel.getLabel()) && !channel.isRemotelyOpened())
channel.sendString("Welcome to " + channel.getSession().getStreamName());
}
/** One whole message. Fan-out is this module's job: WSE applies no routing of its own in module mode. */
public void onWebRTCDataChannelStringMessage(IWebRTCDataChannel channel, String message)
{
if (!CHAT_LABEL.equals(channel.getLabel()))
return;
IWebRTCDataChannelSession sender = channel.getSession();
Set<IWebRTCDataChannelSession> room = rooms.get(sender.getStreamName());
if (room == null)
return;
String line = describe(sender) + ": " + message;
for (IWebRTCDataChannelSession peer : room)
{
if (peer != sender)
sendTo(peer, CHAT_LABEL, line);
}
}
/** This room is text only. The array belongs to the call, so keeping it would mean copying it. */
public void onWebRTCDataChannelBinaryMessage(IWebRTCDataChannel channel, byte[] message)
{
getLogger().debug("ModuleWebRTCDataChannelChatRoom: dropping " + message.length
+ " binary bytes on '" + channel.getLabel() + "'");
}
/** Fires exactly once per channel, whichever side closed it. */
public void onWebRTCDataChannelClose(IWebRTCDataChannel channel)
{
getLogger().debug("ModuleWebRTCDataChannelChatRoom: channel '" + channel.getLabel()
+ "' closed on " + describe(channel.getSession()));
}
/** The connection is gone. Fires once, after every open channel has reported close. */
public void onWebRTCDataChannelSessionDestroy(IWebRTCDataChannelSession session)
{
rooms.computeIfPresent(session.getStreamName(), (streamName, room) -> {
room.remove(session);
return room.isEmpty() ? null : room;
});
getLogger().info("ModuleWebRTCDataChannelChatRoom: " + describe(session) + " left");
}
/** Sends on a session's channel of a given label, if that channel is open. */
private static void sendTo(IWebRTCDataChannelSession session, String label, String message)
{
for (IWebRTCDataChannel channel : session.getChannels())
{
if (label.equals(channel.getLabel()) && channel.isOpen())
{
// Non-blocking and thread-safe, so fanning out from inside a callback is safe.
channel.sendString(message);
return;
}
}
}
private static String describe(IWebRTCDataChannelSession session)
{
return (session.isPublisher() ? "publisher " : "viewer ") + session.getConnectionId();
}
}




