You can inject timed metadata into a live stream by using the Wowza Streaming Engine™ media server software Java API. The metadata, which consists of text that's synchronized to keyframes by a timestamp, allows you to add cued interactivity such as text overlays, bidding, betting, or question-and-answer games to a live stream.
Note: The instructions in this article for injecting AMF metadata into a live stream require the Wowza Streaming Engine Java API 4.5.0 or later.
About injecting timed metadata
To ingest timed metadata, Wowza Streaming Engine uses Action Message Format (AMF), a binary format developed by Adobe for exchanging messages between servers. In a Wowza streaming workflow, AMF metadata, which is delivered over RTMP or WOWZ, is encapsulated in a packet that includes a timecode and text message, such as a caption. Wowza Streaming Engine can receive AMF metadata from an encoder or other source that supports and sends it, or you can inject AMF metadata directly into a stream as it enters the Wowza Streaming Engine server. Injecting AMF metadata at the server level requires using a custom HTTP provider.
Once the AMF metadata is in the stream, it can be converted to ID3 (the format used with HLS for timed metadata) or emsg (the format used with MPEG-DASH for timed metadata) and delivered with the stream when it's played over HLS or MPEG-DASH.
AMF metadata can be of different types—integer, string, or date, for example. For a streaming workflow that involves playing the content using HLS or MPEG-DASH, we recommend using the dictionary type, which creates a JSON string that is easiest to convert to ID3 or emsg.
Create the HTTP provider
In the following code, we create an HTTP provider that injects AMF metadata directly into a live stream as it's received by Wowza Streaming Engine. It uses the com.wowza.wms.amf package of classes, which provide methods for working with AMF metadata, and the IMediaStream interface, which provides access to the stream object.
For this example, Wowza Streaming Engine is broadcasting a live stream of a marathon. When a runner crosses the finish line, his or her name and the time that they cross will appear as an overlay during playback. The HTTP provider will inject the name of the runner and the timestamp into the live stream.
Note: AMF metadata injected in Wowza Streaming Engine can then be injested by Wowza Video. In order for Wowza Video to convert AMF metadata to ID3 tags for HLS playback, it must be in the following format:
- The AMF data must be included in in a top-level AMF data object (AMFDataObj). Within that AMF data object, you can include key:value pairs that include AMF data of additional types, such as string, Boolean, list, or even a nested object.
- The AMFDataObj object must include two properties: (1) a key called payload with a value that is a string of data to be converted and (2) a key called wowzaConverter with a value of basic_string.
Add the following HTTP provider to VHost.xml in the section for port 80:
<HTTPProvider> <BaseClass>com.wowza.wms.plugin.HTTPProviderMarathonDataInjection</BaseClass> <RequestFilters>*marathonData</RequestFilters> <AuthenticationMethod>none</AuthenticationMethod> </HTTPProvider>
To use the provider, make a POST call to Wowza Streaming Engine to add the data to the stream. For example:
http://[Wowza-Streaming-Engine-IP-address]:80/marathonData?application=myApplication&stream=camera1&name=Bessie%20Smith&place=10&time=4%3A10%3A07
Note: See Use HTTP providers with the Wowza Streaming Engine Java API for more information on HTTP providers.
Here's the code for the provider:
package com.wowza.wms.plugin; import java.io.OutputStream; import java.util.Map; import com.wowza.util.*; import com.wowza.wms.amf.*; import com.wowza.wms.application.*; import com.wowza.wms.http.*; import com.wowza.wms.logging.*; import com.wowza.wms.stream.*; import com.wowza.wms.vhost.*; // Usage: POST marathonData?application=myApplication&stream=camera1&name=Bessie%20Smith&place=10&time=4%3A10%3A07 public class HTTPProviderMarathonDataInjection extends HTTPProvider2Base { private static final String CLASSNAME = "HTTPProviderMarathonDataInjection"; private static final Class CLASS = HTTPProviderMarathonDataInjection.class; public void onBind(IVHost vhost, HostPort hostPort) { super.onBind(vhost, hostPort); } public void onHTTPRequest(IVHost vhost, IHTTPRequest req, IHTTPResponse resp) { if (!doHTTPAuthentication(vhost, req, resp)) return; try { // Pull parameters from HTTP POST query string String queryStr = req.getQueryString(); if (queryStr == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Query string missing"); return; } Map queryMap = HTTPUtils.splitQueryStr(queryStr); String appName = queryMap.get("application"); String streamName = queryMap.get("stream"); String appInstanceName = IApplicationInstance.DEFAULT_APPINSTANCE_NAME; String name = queryMap.get("name"); String place = queryMap.get("place"); String time = queryMap.get("time"); if (streamName == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: streamName is null"); return; } if (appName == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: appName is null"); return; } if (name == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: name is null"); return; } if (place == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: place is null"); return; } if (time == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: time is null"); return; } // If / specified int cindex = appName.indexOf("/"); if (cindex > 0) { appInstanceName = appName.substring(cindex+1); appName = appName.substring(0, cindex); } WMSLoggerFactory.getLogger(CLASS).info(CLASSNAME+".onHTTPRequest: stream:"+appName+"/"+appInstanceName+"/"+streamName+" name:"+name+" place:"+place+" time:"+time); // Find application, application instance and stream in running WSE IApplication app = vhost.getApplication(appName); if (app == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Application could not be loaded: "+appName); return; } IApplicationInstance appInstance = app.getAppInstance(appInstanceName); if (appInstance == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: Application instance could not be loaded: "+appName+"/"+appInstanceName); return; } MediaStreamMap streams = appInstance.getStreams(); if (streams == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: No streams: "+appName+"/"+appInstanceName); return; } IMediaStream stream = streams.getStream(streamName); if (stream == null) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: No stream named \""+streamName+"\""); return; } // Inject the finisher data injectFinisherMetadata(stream, name, place, time); // Return HTML Response String response = ""; response += ""; response += "Injected metatdata: Name: "+name + " Place: " + place + " Time:"+time; response += ""; resp.setHeader("Content-Type", "text/html"); OutputStream out = resp.getOutputStream(); out.write(response.getBytes()); } catch (Exception e) { WMSLoggerFactory.getLogger(CLASS).warn(CLASSNAME+".onHTTPRequest: ", e); } } public void injectFinisherMetadata(IMediaStream stream, String name, String place, String time) { try { // Create JSON payload object String payload = createJSONPayload(name, place, time); // Create the AMF data structure // The wowzaConverter property and payload object allow Wowza Video to ingest the AMF metadata and convert it to ID3 AMFDataObj amfData = new AMFDataObj(); amfData.put("wowzaConverter", "basic_string"); amfData.put("payload", payload); WMSLoggerFactory.getLogger(CLASS).info("sendFinisherMetadata ["+stream.getContextStr()+"]"); // Send the data event stream.sendDirect("onRaceFinisher", amfData); } catch(Exception e) { WMSLoggerFactory.getLogger(CLASS).error(CLASSNAME+".sendFinisherMetadata["+stream.getContextStr()+"]: ", e); } } private String createJSONPayload(String name, String place, String time) { return "{\"name\":\""+name+"\", \"place\": "+place+", \"time\": \""+time+"\"}"; } } ,>