How to convert timed metadata from AMF to ID3 using the Wowza Streaming Engine Java API

You can use the Wowza Streaming Engine™ media server software Java API to convert AMF metadata to ID3 metadata tags. This allows metadata ingested with an RTMP source stream to be delivered to streams played using the Apple HLS format.

Note: The instructions in this article for converting AMF metadata to ID3 tags require the Wowza Streaming Engine Server-side Java API 4.5.0 or later.

About converting timed metadata


Producing interactive live streams requires the use of timed metadata. Wowza Streaming Engine ingests timed metadata over RTMP in Action Message Format (AMF), a binary format developed by Adobe for exchanging messages between servers. Wowza Streaming Engine can receive AMF metadata from any H.264 encoder that supports it, or you can inject AMF metadata directly into a stream as it enters the Wowza Streaming Engine server.

Either way, once the AMF metadata is in the stream, it must be converted to ID3 in order for it to be used when the stream is played with Apple HLS in a web browser or mobile app.

Convert timed metadata in a live stream


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 should appear as an overlay in the player.

In the following code, Wowza Streaming Engine listens for an AMF data event, parses it, and maps the event to an ID3 tag. Then it appends the ID3 tag to the appropriate chunk in the HLS stream's .m3u8 HLS playlist.

Note: See com.wowza.wms.amf, com.wowza.wms.httpstreamer.cupertinostream.livestreampacketizer, and com.wowza.wms.media.mp3.model.idtags in the Wowza Streaming Engine Java reference docs for more information on the classes used in this example.

First, add the module to the Modules section of Application.xml:

<Module>
	<Name>ModuleMarathonLiveAMFToID3</Name>
	<Description>Module to convert AMF marathon data to ID3 in live streams</Description>
	<Class>com.wowza.wms.plugin.ModuleCupertinoLiveMarathonAMFToID3</Class>
</Module>

Then use this code for the module:

package com.wowza.wms.plugin;

import com.wowza.wms.amf.*;
import com.wowza.wms.application.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.*;
import com.wowza.wms.media.mp3.model.idtags.*;
import com.wowza.wms.module.*;
import com.wowza.wms.stream.livepacketizer.*;

public class ModuleCupertinoLiveMarathonAMFToID3 extends ModuleBase
{
	class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler2
	{
		private static final String PAYLOAD_TYPE = "onRaceFinisher";

		private LiveStreamPacketizerCupertino liveStreamPacketizer = null;

		public LiveStreamPacketizerDataHandler(LiveStreamPacketizerCupertino liveStreamPacketizer)
		{
			this.liveStreamPacketizer = liveStreamPacketizer;
		}

		public void onFillChunkStart(LiveStreamPacketizerCupertinoChunk chunk)
		{
//			getLogger().info("ModuleCupertinoLiveMarathonAMFToID3.onFillChunkStart["+chunk.getRendition().toString()+":"+liveStreamPacketizer.getContextStr()+"]: chunkId:"+chunk.getChunkIndexForPlaylist());
//
//			// This is how to add custom M3U tag to chunklist header
//			// (Not needed by marathon example, so commented out)
//			CupertinoUserManifestHeaders userManifestHeaders = liveStreamPacketizer.getUserManifestHeaders(chunk.getRendition());
//			if (userManifestHeaders != null)
//				userManifestHeaders.addHeader("MY-USER-HEADER-DATA", "LAST-CHUNK-TIME", (new Date()).toString());

//			// This is how to add ID3 tag to start of chunk
//			// (Not needed by marathon example, so commented out)
//			ID3Frames id3Frames = liveStreamPacketizer.getID3FramesHeader();
//			id3Frames.clear();
//			ID3V2FrameTextInformation comment = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TIT2);
//			comment.setValue("LAST-CHUNK-TIME: "+(new Date()).toString());
//			id3Frames.putFrame(comment);
		}

		public void onFillChunkEnd(LiveStreamPacketizerCupertinoChunk chunk, long timecode)
		{
			// This is where to add ID3 tag to end of chunk
		}

		public void onFillChunkMediaPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet)
		{

		}

		public void onFillChunkDataPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames)
		{
			byte[] buffer = packet.getData();
			if (buffer == null)
				return;

			if (packet.getSize() <= 2)
				return;

			int offset = 0;
			if (buffer[0] == 0)
				offset++;

			AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);

			if (amfList.size() <= 1)
				return;

			if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
				return;

			String metaDataStr = amfList.getString(0);
			AMFDataObj dataObj = amfList.getObject(1);

			if (!metaDataStr.equalsIgnoreCase(PAYLOAD_TYPE))
				return;

			AMFData amfData = dataObj.get("payload");
			if (!(amfData instanceof AMFDataItem))
				return;

			String payload = amfData.toString();

			// Create ID3
			ID3V2FrameTextInformationUserDefined id3 = new ID3V2FrameTextInformationUserDefined();
			id3.setDescription(PAYLOAD_TYPE);
			id3.setValue(payload);
			id3Frames.putFrame(id3);

			getLogger().info("ModuleCupertinoLiveMarathonAMFToID3.onFillChunkDataPacket["+chunk.getRendition().toString()+":"+liveStreamPacketizer.getContextStr()+"] Send payload: "+payload);
		}
	}

	class LiveStreamPacketizerListener extends LiveStreamPacketizerActionNotifyBase
	{
		// When packetizer starts, register as a data handler
		public void onLiveStreamPacketizerCreate(ILiveStreamPacketizer liveStreamPacketizer, String streamName)
		{
			if (liveStreamPacketizer instanceof LiveStreamPacketizerCupertino)
			{
				getLogger().info("ModuleCupertinoLiveOnTextToID3#MyLiveListener.onLiveStreamPacketizerCreate["+((LiveStreamPacketizerCupertino)liveStreamPacketizer).getContextStr()+"]");
				((LiveStreamPacketizerCupertino)liveStreamPacketizer).setDataHandler(new LiveStreamPacketizerDataHandler((LiveStreamPacketizerCupertino)liveStreamPacketizer));
			}
		}
	}

	// When application starts, register to listen to Packetizer events
	public void onAppStart(IApplicationInstance appInstance)
	{
		appInstance.addLiveStreamPacketizerListener(new LiveStreamPacketizerListener());

		getLogger().info("ModuleCupertinoLiveOnTextToID3.onAppStart["+appInstance.getContextStr()+"]");
	}
}

Convert timed metadata for a VOD asset


First, add the module to the Modules section of Application.xml:

<Module>
	<Name>ModuleMarathonVODAMFToID3</Name>
	<Description>Module to convert AMF marathon data to ID3 in VOD streams</Description>
	<Class>com.wowza.wms.plugin.ModuleCupertinoVODMarathonAMFToID3</Class>
</Module>

Then use this code for the module:

package com.wowza.wms.plugin;

import com.wowza.wms.httpstreamer.model.*;
import com.wowza.wms.module.*;
import com.wowza.wms.amf.*;
import com.wowza.wms.application.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.file.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.*;
import com.wowza.wms.media.mp3.model.idtags.*;

public class ModuleCupertinoVODMarathonAMFToID3 extends ModuleBase
{
      private static final String PAYLOAD_TYPE = "onRaceFinisher";

      class VODActionNotify implements IHTTPStreamerCupertinoVODActionNotify2
      {
            IApplicationInstance appInstance = null;
            public VODActionNotify(IApplicationInstance appInstance)
            {
                  this.appInstance = appInstance;
            }

            public void onCreate(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerApplicationContext appContext, IHTTPStreamerSession httpStreamerSession, String rawStreamName, String streamExt, String streamName)
            {
            }

            public void onInit(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerApplicationContext appContext, IHTTPStreamerSession httpStreamerSession, String rawStreamName, String streamExt, String streamName)
            {
            }

            public void onOpen(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerApplicationContext appContext, IHTTPStreamerSession httpStreamerSession, String rawStreamName, String streamExt, String streamName)
            {
            }

            public void onIndex(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerApplicationContext appContext, IHTTPStreamerSession httpStreamerSession, String rawStreamName, String streamExt, String streamName)
            {
            }

            public void onFillChunkStart(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerCupertinoIndexItem item, LiveStreamPacketizerCupertinoChunk chunk, boolean audioOnly)
            {
            }

            public void onFillChunkEnd(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerCupertinoIndexItem item, LiveStreamPacketizerCupertinoChunk chunk, boolean audioOnly)
            {
            }

            public void onDestroy(IHTTPStreamerCupertinoIndex fileIndex)
            {
            }

            public void onFillChunkDataPacket(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerCupertinoIndexItem item, LiveStreamPacketizerCupertinoChunk chunk, boolean audioOnly, AMFPacket packet, ID3Frames id3Frames)
            {
                  byte[] buffer = packet.getData();
                  if (buffer == null)
                        return;

                  if (packet.getSize() <= 2)
                        return;

                  int offset = 0;
                  if (buffer[0] == 0)
                        offset++;

                  AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);

                  if (amfList.size() <= 1)
                        return;

                  if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
                        return;

                  String metaDataStr = amfList.getString(0);
                  AMFDataObj dataObj = amfList.getObject(1);

                  if (!metaDataStr.equalsIgnoreCase(PAYLOAD_TYPE))
                        return;

                  AMFData amfData = dataObj.get("payload");
                  if (!(amfData instanceof AMFDataItem))
                        return;

                  String payload = amfData.toString();

                  // Create ID3
                  ID3V2FrameTextInformationUserDefined id3 = new ID3V2FrameTextInformationUserDefined();
                  id3.setDescription(PAYLOAD_TYPE);
                  id3.setValue(payload);
                  id3Frames.putFrame(id3);

                  getLogger().info("ModuleCupertinoVODMarathonAMFToID3.onFillChunkDataPacket["+appInstance.getContextStr()+"] Send payload: "+payload);
            }
      }

      // When application starts, register to listen to VOD actions
      public void onAppStart(IApplicationInstance appInstance)
      {
            IHTTPStreamerApplicationContext appContext = appInstance.getHTTPStreamerApplicationContext("cupertinostreaming", true);
            if (appContext == null)
                  return;

            if (!(appContext instanceof HTTPStreamerApplicationContextCupertinoStreamer))
                  return;

            HTTPStreamerApplicationContextCupertinoStreamer cupertinoAppContext = (HTTPStreamerApplicationContextCupertinoStreamer)appContext;
            cupertinoAppContext.addVODActionListener(new VODActionNotify(appInstance));

             getLogger().info("ModuleCupertinoVODMarathonAMFToID3.onAppStart["+appInstance.getContextStr()+"]");
      }

Convert timed metadata for an nDVR asset


First, add the module to the Modules section of Application.xml:

<Module>
	<Name>ModuleMarathonDvrAMFToID3</Name>
	<Description>Module to convert AMF marathon data to ID3 in DVR streams</Description>
	<Class>com.wowza.wms.plugin.ModuleCupertinoDVRMarathonAMFToID3</Class>
</Module>

Then use this code for the module:

package com.wowza.wms.plugin;

import com.wowza.wms.amf.*;
import com.wowza.wms.application.*;
import com.wowza.wms.dvr.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.*;
import com.wowza.wms.media.mp3.model.idtags.*;
import com.wowza.wms.module.*;
import com.wowza.wms.stream.livedvr.*;

public class ModuleCupertinoDVRMarathonAMFToID3 extends ModuleBase
{
	private static final String PAYLOAD_TYPE = "onRaceFinisher";

	class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler
	{
		IApplicationInstance appInstance = null;

		public LiveStreamPacketizerDataHandler(IApplicationInstance appInstance)
		{
			this.appInstance = appInstance;
		}

		public void onFillChunkDataPacket(CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames)
		{
			byte[] buffer = packet.getData();
			if (buffer == null)
				return;

			if (packet.getSize() <= 2)
				return;

			int offset = 0;
			if (buffer[0] == 0)
				offset++;

			AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);

			if (amfList.size() <= 1)
				return;

			if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
				return;

			String metaDataStr = amfList.getString(0);
			AMFDataObj dataObj = amfList.getObject(1);

			if (!metaDataStr.equalsIgnoreCase(PAYLOAD_TYPE))
				return;

			AMFData amfData = dataObj.get("payload");
			if (!(amfData instanceof AMFDataItem))
				return;

			String payload = amfData.toString();

			// Create ID3
			ID3V2FrameTextInformationUserDefined id3 = new ID3V2FrameTextInformationUserDefined();
			id3.setDescription(PAYLOAD_TYPE);
			id3.setValue(payload);
			id3Frames.putFrame(id3);

			getLogger().info("ModuleCupertinoVODMarathonAMFToID3.onFillChunkDataPacket["+appInstance.getContextStr()+"] Send payload: "+payload);
		}
	}

	class LiveStreamPacketizerListener implements IDvrStreamManagerActionNotify2
	{
		IApplicationInstance appInstance = null;

		public LiveStreamPacketizerListener(IApplicationInstance appInstance)
		{
			this.appInstance = appInstance;
		}

		@Override
		public void onDvrStreamManagerCreate(IDvrStreamManager dvrMgr)
		{
			getLogger().info("ModuleCupertinoDVRMarathonAMFToID3#LiveStreamPacketizerListener.onDvrStreamManagerCreate["+dvrMgr+"]");

			// When DVR Stream manager is created, register to listen to data handling events
			dvrMgr.setDataHandler(new LiveStreamPacketizerDataHandler(appInstance));
		}

		@Override
		public void onDvrStreamManagerInit(IDvrStreamManager dvrMgr)
		{
		}

		@Override
		public void onDvrStreamManagerDestroy(IDvrStreamManager dvrMgr)
		{
		}

		@Override
		public void onDvrStreamManagerInitStreamName(IDvrStreamManager dvrManager, String streamName)
		{
		}
	}

	// When application starts, register to listen to DVR Packetizer events
	public void onAppStart(IApplicationInstance appInstance)
	{
		appInstance.addDvrStreamManagerListener(new LiveStreamPacketizerListener(appInstance));

		getLogger().info("ModuleCupertinoDVRMarathonAMFToID3.onAppStart["+appInstance.getContextStr()+"]");
	}
}

More resources



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