How to convert onTextData events to ID3 and user manifest data (Apple HLS)

Note: Wowza Streaming Engine 4.3.0.03 or greater is required.

This is new and improved code for live streaming for the following article:

How to convert onTextData events in a live, VOD or nDVR stream to timed events (ID3 tags) in an Apple HLS stream

It has the following improvements:

  • Shows how to add ID3 tags to the header of a chunk
  • Shows how to add custom M3U tags both to the header and inline in chunklist.m3u8
  • Uses newer API to get the ID3 tag container for the header
  • IHTTPStreamerCupertinoLivePacketizerDataHandler2 provides additional context during chunking operation
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()+"]");
	}
}

Hi Charlie,

Is there a way to write the id3tag to the start of the chunked file (hls) as opposed to a timed event.

In a similar fashion to the old MP3 iD3tags.

I have a player that won’t accepted timed events but will read the iD3 tag at the start of the file.

I’m currently using version3 but looking at upgrading to 4 to try this new code out.

Bill

See the updates to the article that I just made. You will need a newer version of Wowza Streaming Engine to get these features. Ask support if you do not have access to the latest private updater.

Charlie

I use adobe flash player and send user data via NetStream.send(“onTextData”, data_obj). This send AMF3 message with type 15. On other side i use flash player and got this message. But on wowza side onFillChunkDataPacket handler never call.