# How to Inject AMF Timed Metadata as SCTE-35 Markers on VOD stream (MP4) via SMIL

**URL:** https://community.wowza.com/t/how-to-inject-amf-timed-metadata-as-scte-35-markers-on-vod-stream-mp4-via-smil/102669
**Category:** Wowza Streaming Engine
**Tags:** wowza-streaming-server-java-api
**Created:** [June 24, 2025, 7:54am UTC](https://community.wowza.com/t/how-to-inject-amf-timed-metadata-as-scte-35-markers-on-vod-stream-mp4-via-smil/102669 "2025-06-24T07:54:00Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![Abdallah\_Monzer](https://avatars.discourse-cdn.com/v4/letter/a/fbc32d/32.png) [@Abdallah\_Monzer](https://community.wowza.com/u/Abdallah_Monzer)
#### Post date: [June 24, 2025, 7:54am UTC](https://community.wowza.com/t/how-to-inject-amf-timed-metadata-as-scte-35-markers-on-vod-stream-mp4-via-smil/102669/1 "2025-06-24T07:54:00Z")

</div>

Hi,

I’m working on a VOD streaming workflow using vod application (not live stream). I have a `sample.mp4` file, and a SMIL file that references it.

- Example playback URL:  
`http://localhost:1935/vod/smil:playlist2.smil/playlist.m3u8`

**Goal:**  
I want to implement **server-side ad insertion (SSAI)** by injecting **AMF Packets** as SCTE-35 markers (on fly) during HLS playback. These markers will be converted to **ID3 tags** for the ad server to recognize and trigger ad replacement (stitch).

**Challenge:**  
The `ModuleAdMarker` and newly added `moduleAdInsertion` in Wowza supports only MPEG-TS sources for live stream.  
In my case, the source is MP4 (referenced via SMIL) and the workflow in vod stream, so these modules don’t work.

**What I’m trying to do:**

- Inject **AMF timed metadata** (SCTE-35) into the stream dynamically during the MP4-to-HLS process.
- These AMF packets should convert into **ID3 tags** within the HLS stream.
- Ideally, the SCTE-35 cue points could be read from a **JSON file** located next to the SMIL file in order to be dynamic.

example of the cue tags must included within the HLS playlist:  
`EXT-X-DATERANGE`, `EXT-X-CUE-IN`, `EXT-X-CUE-OUT` tags, etc..

**Custom Module: `ModuleCueInjector` (Current Approach)**

Here’s what I’ve implemented so far:

- I created a **custom module** , compiled using the Wowza IDE, and placed the `.jar` file in `[Wowza-install-dir]/lib`. Then I added the module reference to `conf/vod/Application.xml` — and the module loads successfully.
- In `onAppStart`, I register a listener (`VODListener`) that implements `IHTTPStreamerCupertinoVODActionNotify2`.
- When a user starts the stream, the `onFillChunkStart` method is triggered every 10 seconds.
- At specific timestamps (e.g., 10s and 30s), I inject **AMF packets** to represent SCTE-35 markers.
- As I understand it, `onFillChunkDataPacket` should then be triggered to convert these AMF packets into **ID3 tags** within the HLS chunks.

**Questions:**

This part is still unclear for me:

- The `onFillChunkDataPacket` is not triggered at all.
- Am I following the correct overall workflow?
- Should I be using a different interface or hook?
- Is this workflow even feasible for MP4-based VOD streams via SMIL?
- Is this feature (dynamic AMF → ID3 injection in VOD via SMIL) officially supported or practical?

```auto
package com.mycompany.wowza;

import com.wowza.wms.httpstreamer.model.*;
import com.wowza.wms.amf.AMFDataItem;
import com.wowza.wms.module.ModuleBase;
import com.wowza.wms.amf.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.HTTPStreamerApplicationContextCupertinoStreamer;
import com.wowza.wms.logging.WMSLogger;
import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession;
import java.util.concurrent.ConcurrentHashMap;
import com.wowza.wms.amf.AMFPacket;
import com.wowza.wms.httpstreamer.cupertinostreaming.file.IHTTPStreamerCupertinoIndex;
import com.wowza.wms.httpstreamer.cupertinostreaming.file.IHTTPStreamerCupertinoIndexItem;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.IHTTPStreamerCupertinoVODActionNotify2;
import com.wowza.wms.httpstreamer.cupertinostreaming.livestreampacketizer.LiveStreamPacketizerCupertinoChunk;
import com.wowza.wms.media.mp3.model.idtags.ID3Frames;
import com.wowza.wms.media.mp3.model.idtags.ID3V2FrameTextInformationUserDefined;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import com.wowza.wms.amf.AMFDataList;
import com.wowza.wms.application.IApplicationInstance;

public class ModuleCueInjector extends ModuleBase {

    public void onAppStart(IApplicationInstance appInstance) {
        String name = appInstance.getApplication().getName() + "/" + appInstance.getName();
        getLogger().info("ModuleCueInjector: onAppStart: " + name);

        IHTTPStreamerApplicationContext appContext = appInstance.getHTTPStreamerApplicationContext("cupertinostreaming", true);
        if (appContext == null)
        {
        	getLogger().warn("ModuleCueInjector#onAppStart: Cupertino streamer application context not found for: " + appInstance.getContextStr());
        	return;
        }
        
        if (appContext instanceof HTTPStreamerApplicationContextCupertinoStreamer) {
            HTTPStreamerApplicationContextCupertinoStreamer cupertinoAppContext = (HTTPStreamerApplicationContextCupertinoStreamer) appContext;
            
            cupertinoAppContext.addVODActionListener(new VODListener(getLogger()));
            getLogger().info("ModuleCueInjector#onAppStart[" + appInstance.getContextStr() + "]: VODListener registered.");
        }
    }
    
    class VODListener implements IHTTPStreamerCupertinoVODActionNotify2 {

        private WMSLogger logger;
        private ConcurrentHashMap<IHTTPStreamerCupertinoIndex, Integer> nextCueIndexMap = new ConcurrentHashMap<>();

		private final String INJECTOR_EVENT_NAME = "onSCTE35Inject";
        private final String SCTE35_CUE_DATA = "/DAIAAAAAAAAAAAQFAAUASABZAP/wBQb+AAAAAAAwAiBDVUVJAAAAAABgAAEBAQABAIAAAAAAAAA=";
        private final double[] CUE_TIMES_SECONDS = {10.0, 30.0};

        public VODListener(WMSLogger logger) {
            this.logger = logger;
        }

        @Override
        public void onOpen(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerApplicationContext appContext, IHTTPStreamerSession httpStreamerSession, String rawStreamName, String streamExt, String streamName) {
            logger.info("ModuleCueInjector.VODListener.onOpen: " + streamName);
            nextCueIndexMap.put(fileIndex, 0);
        }
        
        @Override
        public void onFillChunkStart(
                IHTTPStreamerCupertinoIndex fileIndex,
                IHTTPStreamerCupertinoIndexItem item,
                LiveStreamPacketizerCupertinoChunk chunk,
                boolean audioOnly) {
                logger.info("ModuleCueInjector: onFillChunkStart start: " + item.getStartTimecode());
                logger.info("ModuleCueInjector: onFillChunkStart end: " + item.getStopTimecode());
          	
            Integer nextCueIndex = nextCueIndexMap.get(fileIndex);

            // Stop if we have no more cues to inject for this stream
            if (nextCueIndex == null || nextCueIndex >= CUE_TIMES_SECONDS.length) {
                return;
            }

            double cueTime = CUE_TIMES_SECONDS[nextCueIndex];

            if (item.getStartTimecode() >= (cueTime * 1000)) {
                logger.info("ModuleCueInjector: onFillChunkStart: cue time reached for cue #" + (nextCueIndex + 1) + " at " + cueTime + "s");
                 try {
                    logger.info("ModuleCueInjector: onFillChunkStart: injecting cue: processing..");
 					// Step 1: Create a custom AMF packet to trigger onFillChunkDataPacket
 					AMFDataList amfList = new AMFDataList();
 					amfList.add(new AMFDataItem(INJECTOR_EVENT_NAME));
 					
 					ByteArrayOutputStream baos = new ByteArrayOutputStream();
 					DataOutputStream out = new DataOutputStream(baos);
 					amfList.serialize(out);
 					
 					byte[] dataBytes = baos.toByteArray();
 					
 					// Create a new AMFPacket, set its properties, and add it to the chunk
 					AMFPacket amfPacket = new AMFPacket();
 					amfPacket.setType(18);
 					amfPacket.setDataBuffer(dataBytes);
 					amfPacket.setSize(dataBytes.length);
 					amfPacket.setTimecode(item.getStartTimecode());
 					amfPacket.setAbsTimecode(item.getStartTimecode());
 					
 					// Add the packet to the chunk's list.
 					chunk.addDataPacket(amfPacket);
 					
 					logger.info("ModuleCueInjector: Injected temporary AMF packet at " + item.getStartTimecode() + "ms");
 					
 					// Increment the index to look for the next cue point
 					nextCueIndexMap.put(fileIndex, nextCueIndex + 1);

 				} catch (Exception e) {
 					logger.error("ModuleCueInjector: Failed to inject temporary AMF packet.", e);
 				}
             }else{
                 logger.info("ModuleCueInjector: onFillChunkStart: cue time not reached");
             }
        }

		@Override
		public void onFillChunkDataPacket(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerCupertinoIndexItem item, LiveStreamPacketizerCupertinoChunk chunk, boolean audioOnly, AMFPacket packet, ID3Frames id3Frames) {
        	logger.info("ModuleCueInjector: onFillChunkDataPacket");
			
			byte[] buffer = packet.getData();
			if (buffer == null)
				return;
			
			try {
				AMFDataList amfList = new AMFDataList(buffer);
				
				if (amfList.size() > 0 && amfList.get(0).getType() == AMFData.DATA_TYPE_STRING) {
					String eventName = amfList.getString(0);
					
					// Step 2: Check if this is our custom AMF packet
					if (INJECTOR_EVENT_NAME.equals(eventName)) {
						
						logger.info("ModuleCueInjector: Caught tempofry AMF packet. Converting to ID3 tag.");
						
						// Create the SCTE-35 ID3 frame
						ID3V2FrameTextInformationUserDefined scteFrame = new ID3V2FrameTextInformationUserDefined();
						scteFrame.setDescription("SCTE35-OUT");
						scteFrame.setValue(SCTE35_CUE_DATA);
						
						// Add the frame to the id3Frames container provided by Wowza.
						id3Frames.putFrame(scteFrame);
						
						logger.info("ModuleCueInjector: Successfully converted AMF packet to SCTE-35 ID3 tag.");
					}
				}
			} catch (Exception e) {
				logger.error("ModuleCueInjector: Failed to process AMF data packet.", e);
			}
		}

        @Override
        public void onDestroy(IHTTPStreamerCupertinoIndex fileIndex) {
        	 logger.info("ModuleCueInjector.VODListener.onDestroy");
             if (fileIndex != null) {
                nextCueIndexMap.remove(fileIndex);
             }
        }

		@Override
		public void onFillChunkEnd(IHTTPStreamerCupertinoIndex fileIndex, IHTTPStreamerCupertinoIndexItem item, LiveStreamPacketizerCupertinoChunk chunk, boolean audioOnly) {
		}
    }
}

```

Thanks

---

<div class="post-metadata">

### Author: ![Vihari\_Kumar](https://avatars.discourse-cdn.com/v4/letter/v/87869e/32.png) [@Vihari\_Kumar](https://community.wowza.com/u/Vihari_Kumar)
#### Post date: [June 25, 2025, 11:25am UTC](https://community.wowza.com/t/how-to-inject-amf-timed-metadata-as-scte-35-markers-on-vod-stream-mp4-via-smil/102669/2 "2025-06-25T11:25:06Z")

</div>

Hi,  
If you’re still waiting on an answer here, we recommend opening a formal Wowza support ticket so our team can review your specifics and do our best to assist you directly:  
[How to open a Wowza support ticket](https://www.wowza.com/support/open-ticket)  
If it turns out that your request needs deeper development-level escalation—especially around custom module development or advanced workflows—we may then suggest working with our [Professional Services Team](https://www.wowza.com/professional-services), who specialize in custom solutions tailored to specific project needs.  
Hope this helps! Let us know if you need guidance getting started with a ticket.
