Control the display of program date and time headers in HLS chunklists for Wowza Streaming Engine live streams

The following custom module for Wowza Streaming Engine™ media server software adds the EXT-X-PROGRAM-DATE-TIME header to HLS live streams as well as a TXXX ID3 tag with the same info. EXT-X-PROGRAM-DATE-TIME headers use an absolute date and time tag from the first segment of the stream as the basis for seeking and displaying subsequent segments.

Note: Wowza Streaming Engine™ 4.5.0 or later is required.

To use a custom module, you need to compile it, package it into .jar file, and put it in the [install-dir]/lib folder. Then, add it to your application from the application's Module tab in Wowza Streaming Engine Manager, clicking Add Module, and specifying the module's Name, Description, and Fully Qualified Class Name. See Use Wowza Streaming Engine Java modules for more information.

package com.wowza.wms.plugin.test2.hlsprogramdatetime;

import java.util.*;

import org.apache.commons.lang.time.*;

import com.wowza.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.*;
import com.wowza.wms.stream.livepacketizer.*;

public class ModuleCupertinoProgramDateTime extends ModuleBase
{
    private static final Class<ModuleCupertinoProgramDateTime> CLASS = ModuleCupertinoProgramDateTime.class;
    private static final String CLASSNAME = "ModuleCupertinoProgramDateTime";

    public static final String PROPNAME_TRACKER = "ModuleCupertinoProgramDateTime.ProgramDateTimeTracker";
    public static final String DATEFORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'+00:00'"; // The date/time representation is ISO/IEC 8601:2004 - 2010-02-19T14:54:23.031+08:00

    private FastDateFormat fastDateFormat = FastDateFormat.getInstance(DATEFORMAT, SystemUtils.gmtTimeZone, Locale.US);

    private IApplicationInstance appInstance = null;

    class ProgramDateTimeTracker
    {
        long timeOffset = 0;
        IMediaStream stream = null;

        public ProgramDateTimeTracker(IMediaStream stream)
        {
            this.stream = stream;
        }
    }

    class LiveStreamPacketizerDataHandler implements IHTTPStreamerCupertinoLivePacketizerDataHandler2
    {
        private LiveStreamPacketizerCupertino liveStreamPacketizer = null;
        private int textId = 1;
        private String streamName = null;

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

        public ProgramDateTimeTracker getTracker()
        {
            ProgramDateTimeTracker tracker = null;
            while(true)
            {
                IMediaStream stream = appInstance.getStreams().getStream(this.streamName);
                if (stream == null)
                    break;

                WMSProperties props = stream.getProperties();

                synchronized(props)
                {
                    tracker = (ProgramDateTimeTracker)props.getProperty(PROPNAME_TRACKER);
                    if (tracker == null)
                    {
                        tracker = new ProgramDateTimeTracker(stream);
                        props.put(PROPNAME_TRACKER, tracker);
                    }
                }
                break;
            }

            return tracker;
        }

        @Override
        public void onFillChunkStart(LiveStreamPacketizerCupertinoChunk chunk)
        {
            ProgramDateTimeTracker tracker = getTracker();
            if (tracker != null)
            {
                ElapsedTimer elapsedTime = tracker.stream.getElapsedTime();

                long createTime = elapsedTime.getDate().getTime()+tracker.timeOffset;

                String programDateTimeStr = fastDateFormat.format(new Date(createTime));

                chunk.setProgramDateTime(programDateTimeStr);

                ID3Frames id3Header = liveStreamPacketizer.getID3FramesHeader(chunk.getRendition());
                if (id3Header != null)
                {
                    ID3V2FrameTextInformationUserDefined comment = new ID3V2FrameTextInformationUserDefined();

                    comment.setDescription("programDateTime");
                    comment.setValue(programDateTimeStr);

                    id3Header.clear();
                    id3Header.putFrame(comment);
                }
            }
        }

        @Override
        public void onFillChunkEnd(LiveStreamPacketizerCupertinoChunk chunk, long timecode)
        {
            ProgramDateTimeTracker tracker = getTracker();
            if (tracker != null)
                tracker.timeOffset += chunk.getDuration();
        }

        @Override
        public void onFillChunkDataPacket(LiveStreamPacketizerCupertinoChunk chunk, CupertinoPacketHolder holder, AMFPacket packet, ID3Frames id3Frames)
        {
        }

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

    class LiveStreamPacketizerListener extends LiveStreamPacketizerActionNotifyBase
    {
        public void onLiveStreamPacketizerCreate(ILiveStreamPacketizer liveStreamPacketizer, String streamName)
        {
            if (liveStreamPacketizer instanceof LiveStreamPacketizerCupertino)
            {
                getLogger().info(CLASSNAME+"#MyLiveListener.onLiveStreamPacketizerCreate["+((LiveStreamPacketizerCupertino)liveStreamPacketizer).getContextStr()+"]");
                ((LiveStreamPacketizerCupertino)liveStreamPacketizer).setDataHandler(new LiveStreamPacketizerDataHandler((LiveStreamPacketizerCupertino)liveStreamPacketizer, streamName));
            }
        }
    }

    public void onAppStart(IApplicationInstance appInstance)
    {
        this.appInstance = appInstance;

        appInstance.addLiveStreamPacketizerListener(new LiveStreamPacketizerListener());

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

To enable EXT-X-PROGRAM-DATE-TIME in HLS chunklists, add the cupertinoEnableProgramDateTime property to the application configuration in Wowza Streaming Engine Manager. This property must be true and program date and time set in the chunk for the header to display.

  1. In the Wowza Streaming Engine Manager contents panel, select your live application.
     
  2. On the application page Properties tab, click Custom in the Quick Links bar or scroll to the bottom of the page.
     
    Note: Access to the Properties tab is limited to administrators with advanced permissions. For more information, see Manage credentials.
  3. In the Custom area, click Edit.
     
  4. Click Add Custom Property, and then add the cupertinoEnableProgramDateTime property with the following values:
     
    • Path: Select /Root/Application/HTTPStreamer.
       
    • Name: Enter cupertinoEnableProgramDateTime.
       
    • Type: Select Boolean.
       
    • Value: Enter true.
  5. Click Add, then click Save, and then click Restart when prompted to apply the changes.

More resources