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

Add subFolder AppSettings with mainTrackId and streamId variable support #6833

Merged
merged 15 commits into from
Dec 2, 2024
27 changes: 22 additions & 5 deletions src/main/java/io/antmedia/AppSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -1831,20 +1831,37 @@ public class AppSettings implements Serializable{


/**
* It's S3 streams MP4, WEBM and HLS files storage name .
* It's streams by default.
* Configures the S3 streams folder path for storing media files.
*
* Supports flexible path configurations for different media types including:
* - MP4 files
* - WEBM files
* - HLS (HTTP Live Streaming) files
*
* Path configuration supports dynamic placeholders for HLS files:
* - '%m': Replaces with main track ID if exists
* - '%s': Replaces with stream ID
*
* This is particularly useful for storing conference participant stream hls recordings in separate folders.
*
* Examples of path configurations:
* - "streams" (default) → Basic folder → streams/0001.ts
* - "%m" → Use main track ID as folder → mainTrackId/0001.ts
* - "streams/%m/%s" → Nested folders with track and stream IDs → streams/mainTrackId/streamId/0001.ts
* - "conference/videos/%m/%s" → Custom path with prefixes → conference/videos/mainTrackId/streamId/0001.ts
*
* If main track ID or stream ID are null, they are omitted.
*/
@Value( "${s3StreamsFolderPath:${"+SETTINGS_S3_STREAMS_FOLDER_PATH+":streams}}" )
private String s3StreamsFolderPath="streams";
private String s3StreamsFolderPath="streams";

/**
* It's S3 stream PNG files storage name .
* It's S3 stream PNG files storage name .
* It's previews by default.
*
*/
@Value("${s3PreviewsFolderPath:${"+SETTINGS_S3_PREVIEWS_FOLDER_PATH+":previews}}")
private String s3PreviewsFolderPath="previews";
private String s3PreviewsFolderPath="previews";

/*
* Use http endpoint in CMAF/HLS.
Expand Down
38 changes: 31 additions & 7 deletions src/main/java/io/antmedia/muxer/HLSMuxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
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;
Expand All @@ -15,10 +14,9 @@
import org.bytedeco.ffmpeg.avcodec.*;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.avutil.AVDictionary;
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;
Expand Down Expand Up @@ -61,7 +59,7 @@ public class HLSMuxer extends Muxer {
private String s3StreamsFolderPath = "streams";
private boolean uploadHLSToS3 = true;
private String segmentFilename;

/**
* HLS Segment Type. It can be "mpegts" or "fmp4"
*
Expand Down Expand Up @@ -134,11 +132,10 @@ public void setHlsParameters(String hlsListSize, String hlsTime, String hlsPlayL
/**
* {@inheritDoc}
*/
@Override
public void init(IScope scope, String name, int resolutionHeight, String subFolder, int bitrate) {
public void init(IScope scope, String name, int resolutionHeight, String subFolder, int bitrate, String mainTrackId) {
if (!isInitialized) {

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

streamId = name;
this.subFolder = subFolder;
Expand Down Expand Up @@ -209,12 +206,38 @@ public String getOutputURL()
return super.getOutputURL();
}

public String getCustomHeaderStr() {
String customHeaderStr = "";

if (mainTrackId != null) {
customHeaderStr += "mainTrackId: " + mainTrackId + "\r\n";
}

if (streamId != null) {
customHeaderStr += "streamId: " + streamId + "\r\n";
}

return customHeaderStr;
}

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

outputFormatContext= new AVFormatContext(null);
int ret = avformat_alloc_output_context2(outputFormatContext, null, format, getOutputURL());

if((StringUtils.isNotBlank(httpEndpoint))) {
AVDictionary options = new AVDictionary();
String customHeaderStr = getCustomHeaderStr();
if(StringUtils.isNotBlank(customHeaderStr)){

av_dict_set(options, "headers", customHeaderStr, 0);
lastpeony marked this conversation as resolved.
Show resolved Hide resolved
av_opt_set_dict2(outputFormatContext.priv_data(), options, 0);

}
av_dict_free(options);
}

if (ret < 0) {
logger.info("Could not create output context for {}", getOutputURL());
return null;
Expand Down Expand Up @@ -634,4 +657,5 @@ protected synchronized void clearResource() {
public ByteBuffer getPendingSEIData() {
return pendingSEIData;
}

}
8 changes: 2 additions & 6 deletions src/main/java/io/antmedia/muxer/MuxAdaptor.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.antmedia.muxer;

import static io.antmedia.muxer.IAntMediaStreamHandler.BROADCAST_STATUS_BROADCASTING;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_AAC;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H265;
Expand All @@ -18,15 +17,13 @@
import static org.bytedeco.ffmpeg.global.avutil.av_malloc;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -62,7 +59,6 @@
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IStreamCapableConnection;
import org.red5.server.api.stream.IStreamPacket;
import org.red5.server.net.rtmp.event.AudioData;
import org.red5.server.net.rtmp.event.CachedEvent;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.event.VideoData;
Expand Down Expand Up @@ -525,7 +521,7 @@ public boolean init(IScope scope, String streamId, boolean isAppend) {
}

for (Muxer muxer : muxerList) {
muxer.init(scope, streamId, 0, broadcast.getSubFolder(), 0);
muxer.init(scope, streamId, 0, broadcast.getSubFolder(), 0, null);
}
getStreamHandler().muxAdaptorAdded(this);
return true;
Expand Down Expand Up @@ -2226,7 +2222,7 @@ public boolean prepareMuxer(Muxer muxer, int resolutionHeight)
{
boolean streamAdded = false;

muxer.init(scope, streamId, resolutionHeight, broadcast != null ? broadcast.getSubFolder(): null, 0);
muxer.init(scope, streamId, resolutionHeight, broadcast != null ? broadcast.getSubFolder(): null, 0, null);
logger.info("prepareMuxer for stream:{} muxer:{}", streamId, muxer.getClass().getSimpleName());

if (streamSourceInputFormatContext != null)
Expand Down
42 changes: 24 additions & 18 deletions src/main/java/io/antmedia/muxer/Muxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avformat;
import org.bytedeco.javacpp.BytePointer;
import org.json.simple.JSONObject;
import org.red5.server.api.IContext;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IStreamFilenameGenerator;
Expand Down Expand Up @@ -180,6 +179,7 @@ public abstract class Muxer {
}

protected String subFolder = null;
protected String mainTrackId = null;

/**
* This class is used generally to send direct video buffer to muxer
Expand Down Expand Up @@ -622,40 +622,38 @@ public String getFormat() {
* Inits the file to write. Multiple encoders can init the muxer. It is
* redundant to init multiple times.
*/
public void init(IScope scope, String name, int resolution, String subFolder, int videoBitrate) {
public void init(IScope scope, String name, int resolution, String subFolder, int videoBitrate, String mainTrackId) {
this.streamId = name;
init(scope, name, resolution, true, subFolder, videoBitrate);
init(scope, name, resolution, true, subFolder, videoBitrate, mainTrackId);
}

/**
* Init file name
*
* <p>
* file format is NAME[-{DATETIME}][_{RESOLUTION_HEIGHT}p_{BITRATE}kbps].{EXTENSION}
*
* <p>
* Datetime format is yyyy-MM-dd_HH-mm
*
* <p>
* We are using "-" instead of ":" in HH:mm -> Stream filename must not contain ":" character.
*
* <p>
* sample naming -> stream1-yyyy-MM-dd_HH-mm_480p_500kbps.mp4 if datetime is added
* stream1_480p.mp4 if no datetime
*
* @param name, name of the stream
* @param scope
* @param name,
* name of the stream
* @param resolution
* height of the stream, if it is zero, then no resolution will
* be added to resource name
* @param overrideIfExist
* whether override if a file exists with the same name
* @param bitrate
* bitrate of the stream, if it is zero, no bitrate will
* be added to resource name
* @param resolution height of the stream, if it is zero, then no resolution will
* be added to resource name
* @param overrideIfExist whether override if a file exists with the same name
* @param bitrate bitrate of the stream, if it is zero, no bitrate will
* be added to resource name
* @param mainTrackId
*/
public void init(IScope scope, final String name, int resolution, boolean overrideIfExist, String subFolder, int bitrate) {
public void init(IScope scope, final String name, int resolution, boolean overrideIfExist, String subFolder, int bitrate, String mainTrackId) {
if (!isInitialized) {
isInitialized = true;
this.scope = scope;
this.resolution = resolution;
this.mainTrackId = mainTrackId;

//Refactor: Getting AppSettings smells here
AppSettings appSettings = getAppSettings();
Expand Down Expand Up @@ -1487,4 +1485,12 @@ public String getSubFolder() {
return subFolder;
}

public String getMainTrackId() {
return mainTrackId;
}

public void setMainTrackId(String mainTrackId) {
this.mainTrackId = mainTrackId;
}

}
4 changes: 2 additions & 2 deletions src/main/java/io/antmedia/muxer/RecordMuxer.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public boolean isCodecSupported(int codecId) {
* {@inheritDoc}
*/
@Override
public void init(IScope scope, final String name, int resolutionHeight, String subFolder, int bitrate) {
super.init(scope, name, resolutionHeight, false, subFolder, bitrate);
public void init(IScope scope, final String name, int resolutionHeight, String subFolder, int bitrate, String mainTrackId) {
super.init(scope, name, resolutionHeight, false, subFolder, bitrate, null);

this.streamId = name;
this.resolution = resolutionHeight;
Expand Down
42 changes: 40 additions & 2 deletions src/main/java/io/antmedia/servlet/UploadHLSChunk.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
Expand All @@ -11,6 +13,7 @@
import jakarta.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bytedeco.ffmpeg.avutil.AVDictionary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
Expand All @@ -23,10 +26,11 @@
import io.antmedia.muxer.Muxer;
import io.antmedia.storage.StorageClient;

import static io.antmedia.muxer.Muxer.DATE_TIME_PATTERN;

@MultipartConfig
public class UploadHLSChunk extends HttpServlet {


private static final long serialVersionUID = 1L;

protected static Logger logger = LoggerFactory.getLogger(UploadHLSChunk.class);
Expand Down Expand Up @@ -138,7 +142,41 @@ else if (event.getEventType() == ProgressEventType.TRANSFER_COMPLETED_EVENT)
public static String getS3Key(HttpServletRequest req, AppSettings appSettings) {
//No need have File.separator between the below strings because req.getPathInfo() starts with "/"
//req.getPathInfo(); includes only the part after /hls-upload/. In other words, just {SUB_FOLDER} + (M3U8 or TS files)
return Muxer.replaceDoubleSlashesWithSingleSlash(appSettings.getS3StreamsFolderPath() + File.separator + req.getPathInfo());

String mainTrackId = req.getHeader("mainTrackId");
String streamId = req.getHeader("streamId");

return Muxer.replaceDoubleSlashesWithSingleSlash(getExtendedS3StreamsFolderPath(mainTrackId, streamId, appSettings.getS3StreamsFolderPath()) + File.separator + req.getPathInfo());
}

public static String getExtendedS3StreamsFolderPath(String mainTrackId, String streamId, String s3StreamsFolder) {
if (s3StreamsFolder == null) {
return "";
}

String result = s3StreamsFolder;

if (mainTrackId == null) {
result = result.replace("%m/", "")
.replace("/%m", "")
.replace("%m", "");
} else {
result = result.replace("%m", mainTrackId);
}

if (streamId == null) {
result = result.replace("%s/", "")
.replace("/%s", "")
.replace("%s", "");
} else {
result = result.replace("%s", streamId);
}

result = result.replaceAll("//+", "/");

result = result.trim().replaceAll("^/+|/+$", "");

return result;
}

}
Loading
Loading