Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SEI in HLS #6248

Merged
merged 13 commits into from
May 15, 2024
14 changes: 14 additions & 0 deletions src/main/java/io/antmedia/AppSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,12 @@ public boolean isWriteStatsToDatastore() {
*/
@Value("${id3TagEnabled:false}")
private boolean id3TagEnabled = false;

/**
* Enables the SEI data for HLS
*/
@Value("${seiEnabled:false}")
private boolean seiEnabled = false;

/**
* Ant Media Server can get the audio level from incoming RTP Header in WebRTC streaming and send to the viewers.
Expand Down Expand Up @@ -3584,6 +3590,14 @@ public void setId3TagEnabled(boolean id3TagEnabled) {
this.id3TagEnabled = id3TagEnabled;
}

public boolean isSeiEnabled() {
return seiEnabled;
}

public void setSeiEnabled(boolean seiEnabled) {
this.seiEnabled = seiEnabled;
}

public boolean isSendAudioLevelToViewers() {
return sendAudioLevelToViewers;
}
Expand Down
75 changes: 62 additions & 13 deletions src/main/java/io/antmedia/muxer/HLSMuxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

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.AVMEDIA_TYPE_DATA;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q;
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 org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVCodec;
import org.bytedeco.ffmpeg.avcodec.AVCodecContext;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
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.javacpp.BytePointer;
Expand All @@ -28,6 +26,10 @@

public class HLSMuxer extends Muxer {

public static final String SEI_USER_DATA = "sei_user_data";

private static final String SEI_UUID = "086f3693-b7b3-4f2c-9653-21492feee5b8+";

private static final String SEGMENT_SUFFIX_TS = "%0"+SEGMENT_INDEX_LENGTH+"d.ts";

protected static Logger logger = LoggerFactory.getLogger(HLSMuxer.class);
Expand Down Expand Up @@ -68,6 +70,9 @@ public class HLSMuxer extends Muxer {

private boolean id3Enabled = false;

private boolean seiEnabled = false;


public HLSMuxer(Vertx vertx, StorageClient storageClient, String s3StreamsFolderPath, int uploadExtensionsToS3, String httpEndpoint, boolean addDateTimeToResourceName) {
super(vertx);
this.storageClient = storageClient;
Expand Down Expand Up @@ -351,19 +356,58 @@ public synchronized boolean addStream(AVCodec codec, AVCodecContext codecContext
logger.error("Cannot get codec parameters for {}", streamId);
}

//call super directly because no need to add bit stream filter
//call super directly because no need to add bit stream filter
return super.addStream(codecParameter, codecContext.time_base(), streamIndex);
}

@Override
public synchronized boolean addVideoStream(int width, int height, AVRational timebase, int codecId, int streamIndex,
boolean isAVC, AVCodecParameters codecpar) {

boolean result = super.addVideoStream(width, height, timebase, codecId, streamIndex, isAVC, codecpar);
if (result && seiEnabled)
{
AVStream outStream = getOutputFormatContext().streams(inputOutputStreamIndexMap.get(streamIndex));

setBitstreamFilter("h264_metadata");

AVBSFContext avbsfContext = initVideoBitstreamFilter(getBitStreamFilter(), outStream.codecpar(), inputTimeBaseMap.get(streamIndex));

if (avbsfContext != null) {
int ret = avcodec_parameters_copy(outStream.codecpar(), avbsfContext.par_out());
result = ret == 0;
}

setSeiData("initial sei data");

logger.info("Adding video stream index:{} for stream:", streamIndex);
}

return result;
}

public void setSeiData(String data) {
int ret = av_opt_set(bsfFilterContextList.get(0).priv_data(), SEI_USER_DATA, SEI_UUID+data, AV_OPT_SEARCH_CHILDREN);
logError(ret, "Cannot set sei_user_data for {} and error is {}", streamId);


ret = av_bsf_init(bsfFilterContextList.get(0));
logError(ret, "Cannot update sei_user_data for {} and error is {}", streamId);

}

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)
{
bsfVideoName = "h264_mp4toannexb";

boolean ret = super.addStream(codecParameters, timebase, streamIndex);

return ret;
setBitstreamFilter("h264_mp4toannexb");
return super.addStream(codecParameters, timebase, streamIndex);
}

public boolean addID3Stream() {
Expand Down Expand Up @@ -419,7 +463,12 @@ public String getSegmentFilename() {
public void setId3Enabled(boolean id3Enabled) {
this.id3Enabled = id3Enabled;
}


public void setSeiEnabled(boolean seiEnabled) {
this.seiEnabled = seiEnabled;
}


@Override
protected synchronized void clearResource() {
super.clearResource();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/antmedia/muxer/Mp4Muxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ public synchronized boolean addVideoStream(int width, int height, AVRational tim
*/
@Override
protected boolean prepareAudioOutStream(AVStream inStream, AVStream outStream) {
if (bsfVideoName != null) {
AVBitStreamFilter adtsToAscBsf = av_bsf_get_by_name(this.bsfVideoName);
if (getBitStreamFilter() != null) {
AVBitStreamFilter adtsToAscBsf = av_bsf_get_by_name(this.getBitStreamFilter());
bsfContext = new AVBSFContext(null);

int ret = av_bsf_alloc(adtsToAscBsf, bsfContext);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/antmedia/muxer/MuxAdaptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ public boolean addID3Data(String data) {
return false;
}

public boolean addSEIData(String data) {
for (Muxer muxer : muxerList) {
if(muxer instanceof HLSMuxer) {
((HLSMuxer)muxer).setSeiData(data);
return true;
}
}
return false;
}

public static class PacketTime {
public final long packetTimeMs;
public final long systemTimeMs;
Expand Down Expand Up @@ -433,6 +443,7 @@ public boolean init(IScope scope, String streamId, boolean isAppend) {
hlsMuxer.setHlsParameters( hlsListSize, hlsTime, hlsPlayListType, getAppSettings().getHlsflags(), getAppSettings().getHlsEncryptionKeyInfoFile(), getAppSettings().getHlsSegmentType());
hlsMuxer.setDeleteFileOnExit(deleteHLSFilesOnExit);
hlsMuxer.setId3Enabled(appSettings.isId3TagEnabled());
hlsMuxer.setSeiEnabled(appSettings.isSeiEnabled());
addMuxer(hlsMuxer);
logger.info("adding HLS Muxer for {}", streamId);
}
Expand Down
84 changes: 35 additions & 49 deletions src/main/java/io/antmedia/muxer/Muxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,8 @@
import static org.bytedeco.ffmpeg.global.avformat.avformat_open_input;
import static org.bytedeco.ffmpeg.global.avformat.avformat_write_header;
import static org.bytedeco.ffmpeg.global.avformat.avio_closep;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_AUDIO;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_DATA;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
import static org.bytedeco.ffmpeg.global.avutil.AV_NOPTS_VALUE;
import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_YUV420P;
import static org.bytedeco.ffmpeg.global.avutil.AV_ROUND_NEAR_INF;
import static org.bytedeco.ffmpeg.global.avutil.AV_ROUND_PASS_MINMAX;
import static org.bytedeco.ffmpeg.global.avutil.av_dict_free;
import static org.bytedeco.ffmpeg.global.avutil.av_dict_set;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q_rnd;
import static org.bytedeco.ffmpeg.global.avutil.av_strerror;
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;
Expand All @@ -38,8 +28,6 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.antmedia.FFmpegUtilities;
import io.antmedia.rest.RestServiceBase;
Expand Down Expand Up @@ -133,14 +121,14 @@ public abstract class Muxer {
/**
* Bitstream filter name that will be applied to packets
*/
protected String bsfVideoName = null;
protected List<String> bsfVideoNames = new ArrayList<>();

protected String streamId = null;

protected Map<Integer, AVRational> inputTimeBaseMap = new ConcurrentHashMap<>();


protected AVBSFContext videoBsfFilterContext = null;
protected List<AVBSFContext> bsfFilterContextList = new ArrayList<>();

protected int videoWidth;
protected int videoHeight;
Expand Down Expand Up @@ -464,10 +452,10 @@ protected synchronized void clearResource() {
audioPkt = null;
}

if (videoBsfFilterContext != null) {
for (AVBSFContext videoBsfFilterContext: bsfFilterContextList) {
av_bsf_free(videoBsfFilterContext);
videoBsfFilterContext = null;
}
bsfFilterContextList.clear();

/* close output */
if (outputFormatContext != null &&
Expand Down Expand Up @@ -532,7 +520,7 @@ public void logPacketIssue(String format, Object... arguments) {
/**
* Write packets to the output. This function is used in transcoding.
* Previously, It's the replacement of {link {@link #writePacket(AVPacket)}
* @param avpacket
* @param pkt
* @param codecContext
*/
public synchronized void writePacket(AVPacket pkt, AVCodecContext codecContext) {
Expand Down Expand Up @@ -569,11 +557,15 @@ public ByteBuffer getPacketBufferWithExtradata(byte[] extradata, AVPacket pkt){


public void setBitstreamFilter(String bsfName) {
this.bsfVideoName = bsfName;
bsfVideoNames.add(bsfName);
}

public String getBitStreamFilter() {
return bsfVideoName;
if(!bsfVideoNames.isEmpty())
{
return bsfVideoNames.get(0);
}
return null;
}

public File getFile() {
Expand Down Expand Up @@ -836,16 +828,18 @@ && isCodecSupported(codecParameters.codec_id()) &&
//if it's not running add to the list
registeredStreamIndexList.add(streamIndex);

if (bsfVideoName != null && codecParameters.codec_type() == AVMEDIA_TYPE_VIDEO)
if (codecParameters.codec_type() == AVMEDIA_TYPE_VIDEO)
{
AVBSFContext videoBitstreamFilter = initVideoBitstreamFilter(codecParameters, timebase);
if (videoBitstreamFilter != null)
{
codecParameters = videoBitstreamFilter.par_out();
timebase = videoBitstreamFilter.time_base_out();
for (String bsfVideoName: bsfVideoNames) {
AVBSFContext videoBitstreamFilter = initVideoBitstreamFilter(bsfVideoName, codecParameters, timebase);
if (videoBitstreamFilter != null)
{
codecParameters = videoBitstreamFilter.par_out();
timebase = videoBitstreamFilter.time_base_out();
}
}

}

String codecType = "audio";
if (codecParameters.codec_type() == AVMEDIA_TYPE_VIDEO)
{
Expand Down Expand Up @@ -886,9 +880,9 @@ else if (codecParameters.codec_type() == AVMEDIA_TYPE_DATA)
return result;
}

public AVBSFContext initVideoBitstreamFilter(AVCodecParameters codecParameters, AVRational timebase) {
public AVBSFContext initVideoBitstreamFilter(String bsfVideoName, AVCodecParameters codecParameters, AVRational timebase) {
AVBitStreamFilter bsfilter = av_bsf_get_by_name(bsfVideoName);
videoBsfFilterContext = new AVBSFContext(null);
AVBSFContext videoBsfFilterContext = new AVBSFContext(null);
int ret = av_bsf_alloc(bsfilter, videoBsfFilterContext);

if (ret < 0) {
Expand All @@ -909,6 +903,8 @@ public AVBSFContext initVideoBitstreamFilter(AVCodecParameters codecParameters,
return null;
}

bsfFilterContextList.add(videoBsfFilterContext);

return videoBsfFilterContext;
}

Expand Down Expand Up @@ -1180,32 +1176,22 @@ public void addExtradataIfRequired(AVPacket pkt, boolean isKeyFrame)
protected void writeVideoFrame(AVPacket pkt, AVFormatContext context) {
int ret;


if (videoBsfFilterContext != null)
for(AVBSFContext videoBsfFilterContext : bsfFilterContextList)
{
ret = av_bsf_send_packet(videoBsfFilterContext, pkt);
if (ret < 0) {
logger.warn("Cannot send packet to bit stream filter for stream:{}", streamId);
return;
}
while (av_bsf_receive_packet(videoBsfFilterContext, pkt) == 0)
{
logger.trace("write video packet pts:{} dts:{}", pkt.pts(), pkt.dts());
ret = av_write_frame(context, tmpPacket);
if (ret < 0 && logger.isWarnEnabled()) {
logger.warn("cannot write video frame to muxer({}) av_bsf_receive_packet. Error is {} ", file.getName(), getErrorDefinition(ret));
}
}
ret = av_bsf_receive_packet(videoBsfFilterContext, pkt);
}
else
{
logger.trace("write video packet pts:{} dts:{}", pkt.pts(), pkt.dts());
ret = av_write_frame(context, pkt);
if (ret < 0 && logger.isWarnEnabled()) {
//TODO: this is written for some muxers like HLS because normalized video time is coming from WebRTC
//WebRTCVideoForwarder#getVideoTime. Fix this problem when upgrading the webrtc stack
logger.warn("cannot write video frame to muxer({}). Pts: {} dts:{} Error is {} ", file.getName(), pkt.pts(), pkt.dts(), getErrorDefinition(ret));
}

logger.trace("write video packet pts:{} dts:{}", pkt.pts(), pkt.dts());
ret = av_write_frame(context, pkt);
if (ret < 0 && logger.isWarnEnabled()) {
//TODO: this is written for some muxers like HLS because normalized video time is coming from WebRTC
//WebRTCVideoForwarder#getVideoTime. Fix this problem when upgrading the webrtc stack
logger.warn("cannot write video frame to muxer({}). Pts: {} dts:{} Error is {} ", file.getName(), pkt.pts(), pkt.dts(), getErrorDefinition(ret));
}
}

Expand Down