Add program date-time headers to an HLS chunklist

Follow the steps below to add a EXT-X-PROGRAM-DATE-TIME header and TXXX ID3 to an HLS stream. The EXT-X-PROGRAM-DATE-TIME header uses the absolute date time from the stream's first segment as the basis to seek and display subsequent segments. This article walks you through enabling WSE's built-in PDT headers and includes instructions to enable the custom module used for enabling PDT headers on WSE versions prior to 4.11.

WSE 4.11+ supports built-in program date-time headers. If you are using an earlier version, you must first enable to the custom ModuleCupertinoProgramDateTime module, as described below.

Enable PDT headers


To enable EXT-X-PROGRAM-DATE-TIME in HLS chunklists, add the cupertinoEnableProgramDateTime property to your application's configuration using WSE Manager. This property must be true and program date and time set in the chunk for the header to display.
  1. Navigate to the WSE Manager contents panel.
  2. Select your live app.
  3. Go to the application's Properties tab
  4. 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.
  5. Click Edit.
  6. Click Add Custom Property
  7. Add the cupertinoEnableProgramDateTime property with the following values:
    • Path: Select /Root/Application/HTTPStreamer.
    • Name: Enter cupertinoEnableProgramDateTime.
    • Type: Select Boolean.
    • Value: Enter true.
  8. Click Add.
  9. Click Save.
  10.  Click Restart to apply the changes.

Enable PDT headers using a custom module


The ModuleCupertinoProgramDateTime custom module allowed users to support PDT headers prior to the release of WSE 4.11, which supports built-in PDT headers. To implement PDT headers on a WSE version prior to 4.11, follow the steps below.

  1. Copy the ModuleCupertinoProgramDateTime code below.
  2. Compile it and package it into a .jar file.
  3. Save it to your WSE server's [install-dir]/lib directory.
  4. Add the module to your application.
    1. Navigate to WSE Manager.
    2. Open your app's Module tab.
    3. Click Add Module.
    4. Enter the module's Name, Description, and FQDN
  5. Follow the steps in the Enable PDT headers section (above) to complete the configuration.
// ModuleCupertinoProgramDateTime (to be compiled into a .jar file)

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()+"]");
    }
}

More resources