All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.antmedia.muxer.HLSMuxer Maven / Gradle / Ivy

Go to download

Ant Media Server supports RTMP, RTSP, MP4, HLS, WebRTC, Adaptive Streaming, etc.

The newest version!
package io.antmedia.muxer;

import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avformat.avformat_alloc_output_context2;
import static org.bytedeco.ffmpeg.global.avutil.*;
import static org.bytedeco.ffmpeg.global.avutil.AV_OPT_SEARCH_CHILDREN;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.*;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.avutil.AVRational;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avformat;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacpp.BytePointer;
import org.red5.server.api.scope.IScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.antmedia.storage.StorageClient;
import io.vertx.core.Vertx;

public class HLSMuxer extends Muxer  {

	
	public static final String SEI_USER_DATA = "sei_user_data";

	private static final String SEGMENT_SUFFIX_TS = "%0"+SEGMENT_INDEX_LENGTH+"d.ts";
	private static final String SEGMENT_SUFFIX_FMP4 = "%0"+SEGMENT_INDEX_LENGTH+"d.m4s";
	
	private static final String HLS_SEGMENT_TYPE_MPEGTS = "mpegts";
	private static final String HLS_SEGMENT_TYPE_FMP4 = "fmp4";



	protected static Logger logger = LoggerFactory.getLogger(HLSMuxer.class);
	private String  hlsListSize = "20";
	private String hlsTime = "5";
	private String hlsPlayListType = null;


	private boolean deleteFileOnExit = true;
	private String hlsFlags;
	
	private String segmentInitFilename;;

	private String hlsEncryptionKeyInfoFile = null;

	protected StorageClient storageClient = null;
	private String s3StreamsFolderPath = "streams";
	private boolean uploadHLSToS3 = true;
	private String segmentFilename;
	
	/**
	 * HLS Segment Type. It can be "mpegts" or "fmp4"
	 * 
	 * Note: The generated M3U8 for HEVC can be playable when it's fmp4 
	 * It's not playable when it's mpegts
	 */
	private String hlsSegmentType = HLS_SEGMENT_TYPE_MPEGTS;

	private String httpEndpoint;
	public static final int S3_CONSTANT = 0b010;

	//TODO: make this configurable
	private int id3StreamIndex = 2;
	private AVPacket id3DataPkt;

	private boolean id3Enabled = false;

	private ByteBuffer pendingSEIData;

	private AVPacket tmpPacketForSEI;

	public HLSMuxer(Vertx vertx, StorageClient storageClient, String s3StreamsFolderPath, int uploadExtensionsToS3, String httpEndpoint, boolean addDateTimeToResourceName) {
		super(vertx);
		this.storageClient = storageClient;

		if((S3_CONSTANT & uploadExtensionsToS3) == 0){
			uploadHLSToS3 = false;
		}

		extension = ".m3u8";
		format = "hls";
		firstKeyFrameReceived = false;
		
		firstAudioDts = -1;
		firstVideoDts = -1;

		this.s3StreamsFolderPath  = s3StreamsFolderPath;
		this.httpEndpoint = httpEndpoint;
		setAddDateTimeToSourceName(addDateTimeToResourceName);
	}
	
	public void setHlsParameters(String hlsListSize, String hlsTime, String hlsPlayListType, String hlsFlags, String hlsEncryptionKeyInfoFile, String hlsSegmentType){
		if (hlsListSize != null && !hlsListSize.isEmpty()) {
			this.hlsListSize = hlsListSize;
		}

		if (hlsTime != null && !hlsTime.isEmpty()) {
			this.hlsTime = hlsTime;
		}

		if (hlsPlayListType != null && !hlsPlayListType.isEmpty()) {
			this.hlsPlayListType = hlsPlayListType;
		}

		if (hlsFlags != null && !hlsFlags.isEmpty()) {
			this.hlsFlags = hlsFlags;
		}
		else {
			this.hlsFlags = "";
		}
		if (hlsEncryptionKeyInfoFile != null && !hlsEncryptionKeyInfoFile.isEmpty()) {
			this.hlsEncryptionKeyInfoFile = hlsEncryptionKeyInfoFile;
		}
		
		if (StringUtils.isNotBlank(hlsSegmentType)) {
			this.hlsSegmentType = hlsSegmentType;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void init(IScope scope, String name, int resolutionHeight, String subFolder, int bitrate) {
		if (!isInitialized) {

			super.init(scope, name, resolutionHeight, subFolder, bitrate);

			streamId = name;
			this.subFolder = subFolder;
			options.put("hls_list_size", hlsListSize);
			options.put("hls_time", hlsTime);

			if(hlsEncryptionKeyInfoFile != null) {
				options.put("hls_key_info_file", hlsEncryptionKeyInfoFile);
			}

			logger.info("hls time:{}, hls list size:{} hls playlist type:{} for stream:{}", hlsTime, hlsListSize, this.hlsPlayListType, streamId);

			if (StringUtils.isNotBlank(httpEndpoint)) 			
			{
				
				segmentFilename = httpEndpoint;
				segmentFilename += !segmentFilename.endsWith(File.separator) ? File.separator : "";
				segmentFilename += (this.subFolder != null ? subFolder : "");
				segmentFilename += !segmentFilename.endsWith(File.separator) ? File.separator : "";
				segmentFilename += initialResourceNameWithoutExtension;
				
			}
			else {
				segmentFilename = file.getParentFile().toString();
				segmentFilename += !segmentFilename.endsWith(File.separator) ? File.separator : "";
				segmentFilename += initialResourceNameWithoutExtension;
				
			}
			
			//remove double slashes with single slash because it may cause problems
			segmentFilename = replaceDoubleSlashesWithSingleSlash(segmentFilename);
			
			options.put("hls_segment_type", hlsSegmentType);
			if (HLS_SEGMENT_TYPE_FMP4.equals(hlsSegmentType)) {
				
				segmentInitFilename = initialResourceNameWithoutExtension + "_init.mp4";
				options.put("hls_fmp4_init_filename", segmentInitFilename);
				segmentFilename += SEGMENT_SUFFIX_FMP4;
			}
			else { //if it's mpegts
				segmentFilename += SEGMENT_SUFFIX_TS;
			}
			
					
			options.put("hls_segment_filename", segmentFilename);

			if (hlsPlayListType != null && (hlsPlayListType.equals("event") || hlsPlayListType.equals("vod"))) 
			{
				options.put("hls_playlist_type", hlsPlayListType);
			}

			if (this.hlsFlags != null && !this.hlsFlags.isEmpty()) {
				options.put("hls_flags", this.hlsFlags);
			}
			
			
			tmpPacketForSEI = avcodec.av_packet_alloc();
			isInitialized = true;
		}

	}

	@Override
	public String getOutputURL() 
	{
		if (StringUtils.isNotBlank(httpEndpoint))
		{
			return replaceDoubleSlashesWithSingleSlash(httpEndpoint + File.separator + (this.subFolder != null ? subFolder : "") + File.separator + initialResourceNameWithoutExtension  + extension);
		}
		return super.getOutputURL();
	}


	public AVFormatContext getOutputFormatContext() {
		if (outputFormatContext == null) {

			outputFormatContext= new AVFormatContext(null);
			int ret = avformat_alloc_output_context2(outputFormatContext, null, format, getOutputURL());
			if (ret < 0) {
				logger.info("Could not create output context for {}",  getOutputURL());
				return null;
			}
		}
		return outputFormatContext;
	}

	@Override
	public boolean isCodecSupported(int codecId) {
		return (codecId == AV_CODEC_ID_H264 
				|| codecId == AV_CODEC_ID_AAC  
				|| codecId == AV_CODEC_ID_MP3  
				|| codecId == AV_CODEC_ID_H265 
				|| codecId == AV_CODEC_ID_AC3);
	}
	
	@Override
	public synchronized void writePacket(AVPacket pkt, AVRational inputTimebase, AVRational outputTimebase, int codecType)
	{
			
		if (codecType == AVMEDIA_TYPE_VIDEO && pendingSEIData != null) {
			
			logger.info("sei data size:{} for streamId:{}", pendingSEIData.limit(), streamId);
				
			//inject SEI NAL Unit
			pendingSEIData.rewind();
			int newPacketSize = pkt.size() + pendingSEIData.limit();
						
			av_packet_ref(tmpPacketForSEI, pkt);
			tmpPacketForSEI.position(0);
			
			ByteBuffer packetbuffer = ByteBuffer.allocateDirect(newPacketSize);
			
			packetbuffer.put(pendingSEIData);
			packetbuffer.put(pkt.data().position(0).limit(pkt.size()).asByteBuffer());
			
			packetbuffer.position(0);
			
			tmpPacketForSEI.data(new BytePointer(packetbuffer));
			tmpPacketForSEI.data().position(0).limit(newPacketSize);
			tmpPacketForSEI.size(packetbuffer.limit());			
	
			pendingSEIData = null;
			
			super.writePacket(tmpPacketForSEI, inputTimebase, outputTimebase, codecType);
			
			av_packet_unref(tmpPacketForSEI);
			
		} 
		else {
			super.writePacket(pkt, inputTimebase, outputTimebase, codecType);
		}

	}
	
	/**
	 * We write metadata as ID3 tag for HLS Muxer
	 */
	@Override
	public synchronized void writeMetaData(String data, long dts) {
		addID3Data(data);
	}

	public synchronized void addID3Data(String data) {
		
		
		int id3TagSize = data.length() + 3; // TXXX frame size (excluding 10 byte header)
		int tagSize = id3TagSize + 10;

		ByteBuffer byteBuffer = ByteBuffer.allocate(tagSize + 10);
		
		logger.info("Adding ID3 data: {} lenght:{} byte length:{} buffer capacacity:{}", data, data.length(), data.getBytes().length, byteBuffer.capacity());


		byteBuffer.put("ID3".getBytes());
		byteBuffer.put(new byte[]{0x03, 0x00}); // version
		byteBuffer.put((byte) 0x00); // flags
		byteBuffer.putInt(tagSize); // size

		// TXXX frame
		byteBuffer.put("TXXX".getBytes());
		byteBuffer.putInt(id3TagSize); // size
		byteBuffer.put(new byte[]{0x00, 0x00}); // flags
		byteBuffer.put((byte) 0x03); // encoding
		byteBuffer.put((byte) 0x00); // description 00
		byteBuffer.put(data.getBytes()); // description
		byteBuffer.put((byte) 0x00); // end of string

		byteBuffer.rewind();

		writeID3Packet(byteBuffer);
	}

	public synchronized void writeID3Packet(ByteBuffer data)
	{
		if (!id3Enabled) {
			logger.info("ID3 tag is disabled for stream:{}", streamId);
			return;
		}
		//use the last send video pts as the pts of data
		long pts = getLastPts();
		id3DataPkt.pts(pts);
		id3DataPkt.dts(pts);
		id3DataPkt.stream_index(id3StreamIndex);

		id3DataPkt.data(new BytePointer(data));
		id3DataPkt.size(data.limit());
		id3DataPkt.position(0);

		writeDataFrame(id3DataPkt, getOutputFormatContext());
	}

	@Override
	public boolean writeHeader() {
		createID3StreamIfRequired();
		return super.writeHeader();
	}

	public void createID3StreamIfRequired() {
		if(id3Enabled) {
			logger.info("ID3 tag is enabled for stream:{}", streamId);
			id3DataPkt = avcodec.av_packet_alloc();
			av_init_packet(id3DataPkt);

			addID3Stream();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void writeTrailer() {
		super.writeTrailer();
		
		if (StringUtils.isBlank(this.httpEndpoint)) 
		{
			logger.info("Delete File onexit:{} upload to S3:{} stream:{} hls time:{} hlslist size:{}",
					deleteFileOnExit, uploadHLSToS3, streamId, hlsTime, hlsListSize);
			
			vertx.setTimer(Integer.parseInt(hlsTime) * Integer.parseInt(hlsListSize) * 1000l, l -> 
			{
				final String filenameWithoutExtension = file.getName().substring(0, file.getName().lastIndexOf(extension));
	
				//SEGMENT_SUFFIX_TS is %09d.ts
				//convert segmentFileName to regular expression
				
				int indexOfSuffix = 0;
				if (HLS_SEGMENT_TYPE_FMP4.equals(hlsSegmentType)) {
					indexOfSuffix = segmentFilename.indexOf(SEGMENT_SUFFIX_FMP4);
				}
				else {
					indexOfSuffix = segmentFilename.indexOf(SEGMENT_SUFFIX_TS);
				}
				
				String segmentFileWithoutSuffix = segmentFilename.substring(segmentFilename.lastIndexOf("/")+1, indexOfSuffix);
				String regularExpression = segmentFileWithoutSuffix + "[0-9]*\\.(?:ts|m4s)$";
				File[] files = getHLSFilesInDirectory(regularExpression);
	
				if (files != null)
				{
	
					for (int i = 0; i < files.length; i++) 
					{
						
						handleFinalization(files[i]);
					}
				}
				
				if (segmentInitFilename != null) {
					handleFinalization(new File(file.getParentFile() + File.separator + segmentInitFilename));					
				}
				
				
				
			});
		}
		else {
			logger.info("http endpoint is {} so skipping delete or upload the m3u8 or ts files", httpEndpoint);
		}


	}

	private void handleFinalization(File file) {
		
		try {
			if (uploadHLSToS3 && storageClient.isEnabled()) 
			{
				String path = replaceDoubleSlashesWithSingleSlash(s3StreamsFolderPath + File.separator
						+ (subFolder != null ? subFolder : "") + File.separator + file.getName());
				storageClient.save(path, file, deleteFileOnExit);
			} else if (deleteFileOnExit) {
				Files.deleteIfExists(file.toPath());
			}
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}

	public File[] getHLSFilesInDirectory(String regularExpression) {
		return file.getParentFile().listFiles((dir, name) -> 
		
			//matches m3u8 file or ts segment file
			name.equals(file.getName()) || name.matches(regularExpression)
		);
	}


	@Override
	public synchronized boolean addStream(AVCodec codec, AVCodecContext codecContext, int streamIndex) {
		AVCodecParameters codecParameter = new AVCodecParameters();
		int ret = avcodec_parameters_from_context(codecParameter, codecContext);
		if (ret < 0) {
			logger.error("Cannot get codec parameters for {}", streamId);
		}
		
		//call super directly because no need to add bit stream filter
		return super.addStream(codecParameter, codecContext.time_base(), streamIndex);
	}

	

	public synchronized void setSeiData(String data) {
		
		
		int nbStreams = getOutputFormatContext().nb_streams();
		
		boolean hevcCodec = false;
		boolean h264Codec = false;
		for (int i = 0; i < nbStreams; i++) {
			AVStream stream = getOutputFormatContext().streams(i);
			if (stream.codecpar().codec_type() == AVMEDIA_TYPE_VIDEO) {
				if (stream.codecpar().codec_id() == AV_CODEC_ID_H264) {
					h264Codec = true;
				}
				else if (stream.codecpar().codec_id() == AV_CODEC_ID_H265) {
                    hevcCodec = true;
                }
			}
		}
		
		if (!h264Codec && !hevcCodec) {
            logger.warn("There is no video stream in the muxer, so cannot add SEI data to the muxer. Stream id: {}", streamId);
            return;
		}
		
		//according to the documentation SEI data is  UUID(128bit(16 byte)) + data
		
		// nal unit becomes 00 00 01 + NAL type + SEI type + payload size + payload + align bits
		int length = data.getBytes().length;
		int payloadSize = 16 + length;
		
		int lengthByteCount = payloadSize / 0xff;

		int remaining = payloadSize % 0xff;
		if (remaining != 0) {
			lengthByteCount++;
		}
				
		int totalLength = 4 + 1 + 1 + lengthByteCount + payloadSize + 1;
		

		if (hevcCodec) {
			totalLength += 1; //because of nal unit header is 2 bytes
		}
		pendingSEIData = ByteBuffer.allocateDirect(totalLength);

		
		if (StringUtils.equals(getBitStreamFilter(), BITSTREAM_FILTER_H264_MP4TOANNEXB) || StringUtils.equals(getBitStreamFilter(), BITSTREAM_FILTER_HEVC_MP4TOANNEXB)
				|| HLS_SEGMENT_TYPE_FMP4.equals(hlsSegmentType)) 
		{
			pendingSEIData.putInt(totalLength-4); 

		}
		else {
			pendingSEIData.put((byte)0x00); //start code
			pendingSEIData.put((byte)0x00); //start code
			pendingSEIData.put((byte)0x00); //start code
			pendingSEIData.put((byte)0x01); //start code
		}
		if (h264Codec) {
			pendingSEIData.put((byte) 0x06); // NAL type
		} 
		else 
		{ //HEVC
			pendingSEIData.put((byte) 0x4E); // NAL type
			pendingSEIData.put((byte) 0x01); // NAL type
		}
		pendingSEIData.put((byte)0x05); //SEI type
		
		
		for (int i = 0; i < lengthByteCount-1; i++) {
			pendingSEIData.put((byte) 0xff);
		}
		
		pendingSEIData.put((byte)remaining);  //if payload size is bigger than 0xff, it should be 2 bytes
		
		UUID uuid = UUID.randomUUID();
		
	    pendingSEIData.putLong(uuid.getMostSignificantBits());
	    pendingSEIData.putLong(uuid.getLeastSignificantBits());
        pendingSEIData.put(data.getBytes());		
		pendingSEIData.put((byte)0x80); //RBSP to align the bits
		pendingSEIData.rewind();
		
		
	}
	
	public static void logError(int ret, String message, String streamId) {
		if (ret < 0 && logger.isErrorEnabled()) {
			logger.error(message, streamId, Muxer.getErrorDefinition(ret));
		}
	}
	

	@Override
	public synchronized boolean addStream(AVCodecParameters codecParameters, AVRational timebase, int streamIndex) 
	{
		
		if (codecParameters.codec_id() == AV_CODEC_ID_H264) {
            setBitstreamFilter(BITSTREAM_FILTER_H264_MP4TOANNEXB);
        }
        else if (codecParameters.codec_id() == AV_CODEC_ID_H265){
        	setBitstreamFilter(BITSTREAM_FILTER_HEVC_MP4TOANNEXB);
        }
        else if (codecParameters.codec_id() == AV_CODEC_ID_AAC && HLS_SEGMENT_TYPE_FMP4.equals(hlsSegmentType)) {
        	//we need this conversion for fmp4
        	setAudioBitreamFilter("aac_adtstoasc");
        }
		
		return super.addStream(codecParameters, timebase, streamIndex);
	}

	public boolean addID3Stream() {
		AVCodecParameters codecParameter = new AVCodecParameters();

		codecParameter.codec_type(AVMEDIA_TYPE_DATA);
		codecParameter.codec_id(AV_CODEC_ID_TIMED_ID3);

		return super.addStream(codecParameter, MuxAdaptor.getTimeBaseForMs(), id3StreamIndex);
	}
	
	public String getHlsListSize() {
		return hlsListSize;
	}

	public void setHlsListSize(String hlsListSize) {
		this.hlsListSize = hlsListSize;
	}

	public String getHlsTime() {
		return hlsTime;
	}

	public void setHlsTime(String hlsTime) {
		this.hlsTime = hlsTime;
	}

	public String getHlsPlayListType() {
		return hlsPlayListType;
	}

	public void setHlsPlayListType(String hlsPlayListType) {
		this.hlsPlayListType = hlsPlayListType;
	}

	public boolean isDeleteFileOnExit() {
		return deleteFileOnExit;
	}

	public void setDeleteFileOnExit(boolean deleteFileOnExist) {
		this.deleteFileOnExit = deleteFileOnExist;
	}

	public boolean isUploadingToS3(){
		return uploadHLSToS3;
	}

	public String getSegmentFilename() {
		return segmentFilename;

	}

	public void setId3Enabled(boolean id3Enabled) {
		this.id3Enabled = id3Enabled;
	}

	@Override
	protected synchronized void clearResource() {
		super.clearResource();
		if (id3DataPkt != null) {
			av_packet_free(id3DataPkt);
			id3DataPkt = null;
		}
		
		if (tmpPacketForSEI != null) {
			av_packet_free(tmpPacketForSEI);
			tmpPacketForSEI = null;
		}

	}
	
	public ByteBuffer getPendingSEIData() {
		return pendingSEIData;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy