• How to convert onTextData events in live, VOD, or nDVR streams to timed events (ID3 tags) in Apple HLS streams

    You can convert AMF data events in live and on-demand streams to timed metadata events in Apple HLS (cupertino) streams. Apple HLS supports timed metadata events that are formatted as ID3 tags. Wowza media server software provides an API for listening for AMF data events in live and on-demand streams, parsing the data events, mapping the events to ID3 tags, and then sending the ID3 events through Apple HLS streams. AMF data events in live streams either come directly from the encoder or are injected using IMediaStream.sendDirect. AMF data events in on-demand streams either come from AMF events or subtitle data in the MP4 file.

    Clients can use the iOS API to intercept these events. At this time we don't have detailed examples on how to do this.

    This article provides the following code examples to illustrate how to listen for onTextData events and convert them to ID3 tags:


    On-demand code example - for Wowza Streaming Engine and Wowza Media Server 3 software

    The following code example for on-demand streaming listens for onTextData events and converts them to ID3 tags.
    package com.wowza.wms.plugin.test.module;
    
    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 ModuleCupertinoVODOnTextToID3 extends ModuleBase
    {
    	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)
    		{
    			
    			while(true)
    			{
    				byte[] buffer = packet.getData();
    				if (buffer == null)
    					break;
    				
    				if (packet.getSize() <= 2)
    					break;
    				
    				int offset = 0;
    				if (buffer[0] == 0)
    					offset++;
    				
    				AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);
    				
    				if (amfList.size() <= 1)
    					break;
    				
    				if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
    					break;
    				
    				String metaDataStr = amfList.getString(0);
    				AMFDataObj dataObj = amfList.getObject(1);
    				
    				if (!metaDataStr.equalsIgnoreCase("onTextData"))
    					break;
    				
    				AMFDataItem textData = (AMFDataItem)dataObj.get("text");
    				if (textData == null)
    					break;
    				
    				getLogger().info("ModuleCupertinoVODOnTextToID3.onAppStart["+appInstance.getContextStr()+"] Send string: "+textData.toString());
    								
    				ID3V2FrameTextInformation comment = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TIT2);
    				comment.setValue(textData.toString());
    				id3Frames.putFrame(comment);
    				break;
    			}
    		}
    	}
    
    	public void onAppStart(IApplicationInstance appInstance)
    	{
    		while(true)
    		{
    			IHTTPStreamerApplicationContext appContext = appInstance.getHTTPStreamerApplicationContext("cupertinostreaming", true);
    			if (appContext == null)
    				break;
    			
    			if (!(appContext instanceof HTTPStreamerApplicationContextCupertinoStreamer))
    				break;
    			
    			HTTPStreamerApplicationContextCupertinoStreamer cupertinoAppContext = (HTTPStreamerApplicationContextCupertinoStreamer)appContext;
    			cupertinoAppContext.addVODActionListener(new VODActionNotify(appInstance));
    			break;
    		}
    		
    		getLogger().info("ModuleCupertinoVODOnTextToID3.onAppStart["+appInstance.getContextStr()+"]");
    	}
    }

    Live code example - for Wowza Streaming Engine software (version 4.4.0 and later)

    The following code example for live streaming listens for onTextData events and converts them to ID3 tags.
    package com.wowza.wms.plugin.test.module;
    
    import java.util.*;
    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 ModuleCupertinoLiveOnTextToID3 extends ModuleBase
    {
    	class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler2
    	{
    		private LiveStreamPacketizerCupertino liveStreamPacketizer = null;
    		private int textId = 1;
    		
    		public LiveStreamPacketizerDataHandler(LiveStreamPacketizerCupertino liveStreamPacketizer)
    		{
    			this.liveStreamPacketizer = liveStreamPacketizer;
    		}
    
    		public void onFillChunkStart(LiveStreamPacketizerCupertinoChunk chunk)
    		{
    			getLogger().info("ModuleCupertinoVODOnTextToID3.onFillChunkStart["+chunk.getRendition().toString()+":"+liveStreamPacketizer.getContextStr()+"]: chunkId:"+chunk.getChunkIndexForPlaylist());
    			
    			// Add custom M3U tag to chunklist header
    			CupertinoUserManifestHeaders userManifestHeaders = liveStreamPacketizer.getUserManifestHeaders(chunk.getRendition());
    			if (userManifestHeaders != null)
    				userManifestHeaders.addHeader("MY-USER-HEADER-DATA", "LAST-CHUNK-TIME", (new Date()).toString());
    			
    			// Add ID3 tag to start of chunk
    			ID3Frames id3Frames = liveStreamPacketizer.getID3FramesHeader();
    			ID3V2FrameTextInformation comment = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TIT2);
    			comment.setValue("LAST-CHUNK-TIME: "+(new Date()).toString());
    			id3Frames.clear();
    			id3Frames.putFrame(comment);
    
    			textId = 1;
    		}
    		
    		public void onFillChunkEnd(LiveStreamPacketizerCupertinoChunk chunk, long timecode)
    		{
    			getLogger().info("ModuleCupertinoVODOnTextToID3.onFillChunkEnd["+chunk.getRendition().toString()+":"+liveStreamPacketizer.getContextStr()+"]: chunkId:"+chunk.getChunkIndexForPlaylist());
    		}
    		
    		public void onFillChunkMediaPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet)
    		{
    			
    		}
    
    		public void onFillChunkDataPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames)
    		{			
    			while(true)
    			{
    				byte[] buffer = packet.getData();
    				if (buffer == null)
    					break;
    				
    				if (packet.getSize() <= 2)
    					break;
    				
    				int offset = 0;
    				if (buffer[0] == 0)
    					offset++;
    				
    				AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);
    				
    				if (amfList.size() <= 1)
    					break;
    				
    				if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
    					break;
    				
    				String metaDataStr = amfList.getString(0);
    				AMFDataObj dataObj = amfList.getObject(1);
    								
    				if (!metaDataStr.equalsIgnoreCase("onTextData"))
    					break;
    				
    				AMFDataItem textData = (AMFDataItem)dataObj.get("text");
    				if (textData == null)
    					break;
    				
    				String textStr = textData.toString();
    				
    				getLogger().info("ModuleCupertinoVODOnTextToID3.onFillChunkDataPacket["+chunk.getRendition().toString()+":"+liveStreamPacketizer.getContextStr()+"] Send string: "+textStr);
    						
    				// Add ID3 tag with text data in chunk based on timecode order
    				ID3V2FrameTextInformation comment = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TIT2);
    				comment.setValue(textStr);
    				id3Frames.putFrame(comment);
    				
    				// Add custom M3U tag to chunklist just above chunk
    				CupertinoUserManifestHeaders userManifestHeaders = chunk.getUserManifestHeaders();
    				if (userManifestHeaders != null)
    					userManifestHeaders.addHeader("MY-USER-CHUNK-DATA-"+textId, "ON-TEXT-DATA", textStr, true);
    				
    				textId++;
    				break;
    			}
    		}
    	}
    	
    	class LiveStreamPacketizerListener extends LiveStreamPacketizerActionNotifyBase
    	{
    		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));
    			}
    		}
    	}
    
    	public void onAppStart(IApplicationInstance appInstance)
    	{		
    		appInstance.addLiveStreamPacketizerListener(new LiveStreamPacketizerListener());
    
    		getLogger().info("ModuleCupertinoLiveOnTextToID3.onAppStart["+appInstance.getContextStr()+"]");
    	}
    }

    nDVR code example - for Wowza Streaming Engine software (version 4.3.0 and later)

    The following code example for DVR streaming listens for onTextData events and converts them to ID3 tags.
    package com.wowza.wms.plugin.test.module;
    
    
    import com.wowza.wms.amf.AMFData;
    import com.wowza.wms.amf.AMFDataItem;
    import com.wowza.wms.amf.AMFDataList;
    import com.wowza.wms.amf.AMFDataObj;
    import com.wowza.wms.amf.AMFPacket;
    import com.wowza.wms.application.IApplicationInstance;
    import com.wowza.wms.dvr.IDvrStreamManager;
    import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.CupertinoPacketHolder;
    import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.IHTTPStreamerCupertinoLivePacketizerDataHandler;
    import com.wowza.wms.media.mp3.model.idtags.ID3Frames;
    import com.wowza.wms.media.mp3.model.idtags.ID3V2FrameBase;
    import com.wowza.wms.media.mp3.model.idtags.ID3V2FrameTextInformation;
    import com.wowza.wms.module.ModuleBase;
    import com.wowza.wms.stream.livedvr.IDvrStreamManagerActionNotify2;
    
    
    public class ModuleDvrDataToID3 extends ModuleBase
    {
    	class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler
    	{
    		IApplicationInstance appInstance = null;
    		
    		public LiveStreamPacketizerDataHandler(IApplicationInstance appInstance)
    		{
    			this.appInstance = appInstance;
    		}
    
    		public void onFillChunkDataPacket(CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames)
    		{			
    			while(true)
    			{
    				byte[] buffer = packet.getData();
    				if (buffer == null)
    					break;
    				
    				if (packet.getSize() <= 2)
    					break;
    				
    				int offset = 0;
    				if (buffer[0] == 0)
    					offset++;
    				
    				AMFDataList amfList = new AMFDataList(buffer, offset, buffer.length-offset);
    				
    				if (amfList.size() <= 1)
    					break;
    				
    				if (amfList.get(0).getType() != AMFData.DATA_TYPE_STRING && amfList.get(1).getType() != AMFData.DATA_TYPE_OBJECT)
    					break;
    				
    				String metaDataStr = amfList.getString(0);
    				System.out.println("meta:"+metaDataStr);
    				
    				AMFDataObj dataObj = amfList.getObject(1);
    				
    				if (!metaDataStr.equalsIgnoreCase("onTextData"))
    					break;
    				
    				AMFDataItem textData = (AMFDataItem)dataObj.get("text");
    				if (textData == null)
    					break;
    				
    				getLogger().info("ModuleDvrDataToID3.onAppStart["+appInstance.getContextStr()+"] Send string: "+textData.toString());
    								
    				ID3V2FrameTextInformation comment = new ID3V2FrameTextInformation(ID3V2FrameBase.TAG_TIT2);
    				comment.setValue(textData.toString());
    				id3Frames.putFrame(comment);
    				break;
    			}
    		}
    	}
    	
    	class LiveStreamPacketizerListener implements IDvrStreamManagerActionNotify2
    	{
    		IApplicationInstance appInstance = null;
    		
    		public LiveStreamPacketizerListener(IApplicationInstance appInstance)
    		{
    			this.appInstance = appInstance;
    		}
    
    
    		@Override
    		public void onDvrStreamManagerCreate(IDvrStreamManager dvrMgr)
    		{
    			getLogger().info("ModuleDvrDataToID3#MyLiveListener.onDvrStreamManagerCreate["+dvrMgr+"]");
    			
    			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)
    		{
    		}
    	}
    
    	public void onAppStart(IApplicationInstance appInstance)
    	{		
    		appInstance.addDvrStreamManagerListener(new LiveStreamPacketizerListener(appInstance));
    
    		getLogger().info("ModuleDvrDataToID3.onAppStart["+appInstance.getContextStr()+"]");
    	}
    }

    More resources

    For information about a method for debugging ID3 tags in iOS streams, see How to debug timed data events in iOS streams (ID3 tags).
    Originally Published: 10-10-2011.
    Updated: For Wowza Streaming Engine 4.4.0 on 02-03-2016.

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