How to add support for a single file with multiple tracks for any HLS Protocol

A question we have received many times over the last several months is can you get Wowza Streaming Engine to support single files with multiple tracks for any HLS Protocol.

We have created a workaround for this lack of support via this module. It does require some ‘special’ SMIL file entries as it appears query parameters do not work in SMIL files, so we have replaced them with ##.

An example SMIL file that would be used is as follows

<?xml version="1.0" encoding="UTF-8"?>
<smil title="">
<body>
	<switch>
		<video height="240" src="file.mp4##videoindex=0&amp;audioindex=0" system-language="en" title="English" width="424">
			<param name="videoBitrate" value="450000" valuetype="data"></param>
			<param name="audioBitrate" value="64000" valuetype="data"></param>
		</video>
		<video height="360" src="file.mp4##audioindex=1" system-language="gr" title="SomethingElse" audio-bitrate="64000">
			<param name="audioOnly" value="TRUE" valuetype="data"/>
		</video>
	</switch>
</body>
</smil>

You can see we have replaced the ? with ##. It should also be noted that the ampersand sign needs to be specified in html form.

In order to use the above special SMIL file the following code has been created.

A media reader listener that takes the query parameters provided and then sets the appropriate track selections on a file.

ModuleHLSMultiTrack Class

package guru.thewowza.example.multitrackhls;

import java.util.*;
import com.wowza.util.*;
import com.wowza.wms.module.*;
import com.wowza.wms.application.*;
import com.wowza.wms.stream.*;
import com.wowza.wms.mediareader.h264.*;

public class ModuleHLSMultiTrack extends ModuleBase
	{
	public static final String PROPERTY_AUDIOINDEX = "audioindex";
	public static final String PROPERTY_VIDEOINDEX = "videoindex";
	public static final String PROPERTY_DATAINDEX = "dataindex";
	public static final String[] PROPERTY_INDEXES = {PROPERTY_AUDIOINDEX, 
						PROPERTY_VIDEOINDEX, PROPERTY_DATAINDEX};

	class MediaReaderListener implements IMediaReaderActionNotify
		{ 
		public void onMediaReaderCreate(IMediaReader mediaReader)
			{
			}

		public void onMediaReaderInit(IMediaReader mediaReader, IMediaStream stream)
			{			
			Map<String, String> queryParams = HTTPUtils.splitQueryStr(stream.getProperties().getPropertyStr("queryString", "none"));
			for(int i=0;i<PROPERTY_INDEXES.length;i++)
				{
				String indexStr = PROPERTY_INDEXES[i];
				if (queryParams.containsKey(indexStr))
					{
					int index = -1;
					try
						{
						index = Integer.parseInt(queryParams.get(indexStr));
						}
						catch(Exception e)
						{
						}
					if (index >= 0)
						{
						stream.getProperties().setProperty(indexStr, new Integer(index));
						}
					}
				}
			}

		public void onMediaReaderOpen(IMediaReader mediaReader, IMediaStream stream)
			{
			while(true)
				{
				int audioindex = -1;
				int videoIndex = -1;
				int dataIndex = -1;
				Integer audioindexObj = (Integer)stream.getProperties().getProperty(PROPERTY_AUDIOINDEX);                                                 
				Integer videoIndexObj = (Integer)stream.getProperties().getProperty(PROPERTY_VIDEOINDEX);
				Integer dataIndexObj = (Integer)stream.getProperties().getProperty(PROPERTY_DATAINDEX);

				if (audioindexObj != null)
					audioindex = audioindexObj.intValue();
				if (videoIndexObj != null)
					videoIndex = videoIndexObj.intValue();
				if (dataIndexObj != null)
					dataIndex = dataIndexObj.intValue();

				if (mediaReader instanceof MediaReaderH264)
					{
					MediaReaderH264 mediaReaderH264 = (MediaReaderH264)mediaReader;
					if (audioindex >= 0)
						{
						mediaReaderH264.setTrackIndexAudio(audioindex);
						}
					if (videoIndex >= 0)
						{
						mediaReaderH264.setTrackIndexVideo(videoIndex);
						}
					if (dataIndex >= 0)
						{
						mediaReaderH264.setTrackIndexData(dataIndex);
						mediaReaderH264.setTrackDataCharSet("UTF-8");
						}
					}                
				break;
				}
			}

		public void onMediaReaderExtractMetaData(IMediaReader mediaReader, IMediaStream stream)
			{
			}

		public void onMediaReaderClose(IMediaReader mediaReader, IMediaStream stream)
			{
			}
		}

	public void onAppStart(IApplicationInstance appInstance)
		{
		appInstance.addMediaReaderListener(new MediaReaderListener());
		}
 	}

In order for this class to work we also needed to create a MediaReader Class

MediaReaderHLSMultiTrack Class

package guru.thewowza.example.multitrackhls;

import java.io.*;
import com.wowza.io.*;
import com.wowza.util.*;
import com.wowza.wms.application.*;
import com.wowza.wms.stream.*;

public class MediaReaderHLSMultiTrack implements IRandomAccessReader, ITrackRandomAccessReaderPerformance
	{
	private IRandomAccessReader randomReader = null;
	String queryString = "";

	public void init(IApplicationInstance appInstance, IMediaStream stream, String basePath, String mediaName, String mediaExtension)
		{
		randomReader = new DirectRandomAccessReader();
		if ( !mediaName.endsWith(".smil") )
			{
			try 
				{
				queryString = mediaName.split("##")[1];	
				} 
				catch (Exception queryFail) { queryString=""; }
			if ( queryString.length()>0 )
				{
				mediaName = mediaName.substring( 0, mediaName.indexOf(queryString)-2 );
				}
			}
		stream.getProperties().setProperty("queryString", queryString);
		randomReader.init(appInstance, stream, basePath, mediaName, mediaExtension);
		}

	public void setStreamIOTracker(IOPerformanceCounter ioPerforamnceCounter)
	{
		if (randomReader instanceof ITrackRandomAccessReaderPerformance)
			((ITrackRandomAccessReaderPerformance)randomReader).setStreamIOTracker(ioPerforamnceCounter);
	}

	public void setClientIOTracker(IOPerformanceCounter ioPerforamnceCounter)
	{
		if (randomReader instanceof ITrackRandomAccessReaderPerformance)
			((ITrackRandomAccessReaderPerformance)randomReader).setClientIOTracker(ioPerforamnceCounter);
	}

	public void open() throws IOException
	{
		randomReader.open();
	}

	public void close() throws IOException
	{
		randomReader.close();
	}

	public boolean isOpen()
	{
		return randomReader.isOpen();
	}

	public long getFilePointer()
	{
		return randomReader.getFilePointer();
	}

	public void seek(long pos)
	{
		randomReader.seek(pos);
	}

	public int read(byte[] buf, int off, int size)
	{
		return randomReader.read(buf, off, size);
	}

	public int getDirecton()
	{
		return randomReader.getDirecton();
	}

	public void setDirecton(int directon)
	{
		randomReader.setDirecton(directon);
	}

	public String getBasePath()
	{
		return randomReader.getBasePath();
	}

	public String getMediaName()
	{
		return randomReader.getMediaName();
	}

	public String getMediaExtension()
	{
		return randomReader.getMediaExtension();
	}

	public boolean exists()
	{
		return randomReader.exists();
	} 

	public long lastModified()
	{
		return randomReader.lastModified();
	}

	public long length()
	{
		return randomReader.length();
	}

	public String getPath()
	{
		return randomReader.getPath();
	}
}

To make these two class work the following should be added to your Application.xml

A property in the MediaReader section so it would looking like this

<MediaReader>
	<Properties>
		<Property>
			<Name>randomAccessReaderClass</Name>
			<Value>guru.thewowza.example.multitrackhls.MediaReaderHLSMultiTrack</Value>
			<Type>String</Type>
		</Property>
	</Properties>
</MediaReader>

You also would need to add a module entry as the last module in the Modules section of the Application.xml which would look like this

<Module>
	<Name>Reader</Name>
	<Description>Reader</Description>
	<Class>guru.thewowza.example.multitrackhls.ModuleHLSMultiTrack</Class>
</Module>

You should now be able to make HLS requests for the SMIL file in the same fashion as you do now examples being

Cupertino


http://127.0.0.1:1935/vod/testing.smil/playlist.m3u8

MPEG DASH


http://127.0.0.1:1935/vod/testing.smil/manifest.mpd

Silverlight


http://127.0.0.1:1935/vod/testing.smil/Manifest

Download the moduleĀ here.


Comments are closed.