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

The following module for Wowza Streaming Engine™ media server software adds the EXT-X-PROGRAM-DATE-TIME header to Apple HLS (Cupertino) live streams as well as a TXXX ID3 tag with the same information. 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 enable EXT-X-PROGRAM-DATE-TIME headers in Apple HLS chunk lists, add the module code to the Wowza Streaming Engine live application and then use Wowza Streaming Engine Manager to add the cupertinoEnableProgramDateTime property to the application configuration. This property must be true and program date and time set in the chunk for the header to display.

  1. Compile the following module code into a .jar file and add it to the Wowza Streaming Engine live application.
    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()+"]");
        }
    }
  2. In the Wowza Streaming Engine Manager contents panel, click the live application name (for example, live).
     
  3. On the live 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.
  4. In the Custom area, click Edit.
     
  5. 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.
  6. Click Add, then click Save.
  7. When prompted, click Restart to apply the changes.