How to inject timed metadata using a Wowza Streaming Engine HTTP provider

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 Server-side 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, or chunk, that includes a timecode and text message, such as a caption. Wowza Streaming Engine can receive AMF metadata from an encoder or other H.264 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 container format used by Apple HLS for timed metadata, and delivered with the stream when it's played over HLS in a web browser or mobile app.

AMF metadata can be of different types—integer, string, or date, for example. For a streaming workflow that involves playing the content using HLS, we recommend using the dictionary type, which creates a JSON string that is easiest to convert to ID3.

Create the HTTP provider


In the following code, we create a 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.

Add the following HTTP provider to VHost.xml in the section for port 80:

<HTTPProvider>
    <BaseClass>com.wowza.example.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=live&stream=camera1&name=Bessie%20Smith&place=10&time=4%3A10%3A07

Note: See How to create an HTTP provider and How to use server-side modules and HTTP providers 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=live&stream=camera1&name=Bessie%20Smith&place=10&time=4%3A10%3A07
public class HTTPProviderMarathonDataInjection extends HTTProvider2Base
{
	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 AMF structure
			// Using this format of AMF Data will allow WSC to automatically convert AMF 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+"\"}";
	}
}
,>

More resources



If you're having problems or want to discuss this article, post in our forum.