io.antmedia.AntMediaApplicationAdapter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ant-media-server Show documentation
Show all versions of ant-media-server Show documentation
Ant Media Server supports RTMP, RTSP, MP4, HLS, WebRTC, Adaptive Streaming, etc.
The newest version!
package io.antmedia;
import static io.antmedia.muxer.IAntMediaStreamHandler.BROADCAST_STATUS_BROADCASTING;
import static org.bytedeco.ffmpeg.global.avcodec.avcodec_get_name;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.red5.server.adapter.MultiThreadedApplicationAdapter;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IClientBroadcastStream;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IStreamCapableConnection;
import org.red5.server.api.stream.IStreamPublishSecurity;
import org.red5.server.api.stream.ISubscriberStream;
import org.red5.server.stream.ClientBroadcastStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.antmedia.analytic.model.PublishEndedEvent;
import io.antmedia.analytic.model.PublishStartedEvent;
import io.antmedia.analytic.model.ViewerCountEvent;
import io.antmedia.cluster.ClusterNode;
import io.antmedia.cluster.IClusterNotifier;
import io.antmedia.datastore.db.DataStore;
import io.antmedia.datastore.db.DataStoreFactory;
import io.antmedia.datastore.db.types.Broadcast;
import io.antmedia.datastore.db.types.BroadcastUpdate;
import io.antmedia.datastore.db.types.VoD;
import io.antmedia.datastore.preference.PreferenceStore;
import io.antmedia.filter.StreamAcceptFilter;
import io.antmedia.ipcamera.OnvifCamera;
import io.antmedia.logger.LoggerUtils;
import io.antmedia.muxer.IAntMediaStreamHandler;
import io.antmedia.muxer.MuxAdaptor;
import io.antmedia.muxer.Muxer;
import io.antmedia.plugin.api.IClusterStreamFetcher;
import io.antmedia.plugin.api.IFrameListener;
import io.antmedia.plugin.api.IPacketListener;
import io.antmedia.plugin.api.IStreamListener;
import io.antmedia.rest.RestServiceBase;
import io.antmedia.rest.model.Result;
import io.antmedia.security.AcceptOnlyStreamsInDataStore;
import io.antmedia.settings.ServerSettings;
import io.antmedia.shutdown.AMSShutdownManager;
import io.antmedia.shutdown.IShutdownListener;
import io.antmedia.statistic.DashViewerStats;
import io.antmedia.statistic.HlsViewerStats;
import io.antmedia.statistic.ViewerStats;
import io.antmedia.statistic.type.RTMPToWebRTCStats;
import io.antmedia.statistic.type.WebRTCAudioReceiveStats;
import io.antmedia.statistic.type.WebRTCAudioSendStats;
import io.antmedia.statistic.type.WebRTCVideoReceiveStats;
import io.antmedia.statistic.type.WebRTCVideoSendStats;
import io.antmedia.storage.StorageClient;
import io.antmedia.streamsource.StreamFetcher;
import io.antmedia.streamsource.StreamFetcherManager;
import io.antmedia.track.ISubtrackPoller;
import io.antmedia.webrtc.PublishParameters;
import io.antmedia.webrtc.api.IWebRTCAdaptor;
import io.antmedia.webrtc.api.IWebRTCClient;
import io.antmedia.websocket.WebSocketConstants;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.dropwizard.MetricsService;
import jakarta.validation.constraints.NotNull;
public class AntMediaApplicationAdapter extends MultiThreadedApplicationAdapter implements IAntMediaStreamHandler, IShutdownListener {
public static final String BEAN_NAME = "web.handler";
public static final int BROADCAST_STATS_RESET = 0;
public static final String HOOK_ACTION_END_LIVE_STREAM = "liveStreamEnded";
public static final String HOOK_ACTION_START_LIVE_STREAM = "liveStreamStarted";
public static final String HOOK_ACTION_STREAM_STATUS = "liveStreamStatus";
public static final String HOOK_ACTION_VOD_READY = "vodReady";
public static final String HOOK_ACTION_PUBLISH_TIMEOUT_ERROR = "publishTimeoutError";
public static final String HOOK_ACTION_ENCODER_NOT_OPENED_ERROR = "encoderNotOpenedError";
public static final String HOOK_ACTION_ENDPOINT_FAILED = "endpointFailed";
/**
* This is used to notify that the play is stopped
*/
public static final String HOOK_ACTION_PLAY_STOPPED = "playStopped";
/**
* This is used to notify that the play is started
*/
public static final String HOOK_ACTION_PLAY_STARTED = "playStarted";
/**
* This is used to notify that the subtrack is created in the main track
* In video conferencing, it means a stream is started in the room
*/
public static final String HOOK_ACTION_SUBTRACK_ADDED_IN_THE_MAINTRACK = "subtrackAddedInTheMainTrack";
/**
* This is used to notify that the subtrack left the main track
* In video conferencing, it means a stream left the room
*/
public static final String HOOK_ACTION_SUBTRACK_LEFT_FROM_THE_MAINTRACK = "subtrackLeftTheMainTrack";
/**
* This is used to notify that the first active subtrack is created in the main track
* In video conferencing, it means the first stream is started in the room
*/
public static final String HOOK_ACTION_FIRST_ACTIVE_SUBTRACK_ADDED_IN_THE_MAINTRACK = "firstActiveTrackAddedInMainTrack";
/**
* This is used to notify that there is no active subtracks left in the main track.
* In video conferencing, it means there is no active stream left in the room
*/
public static final String HOOK_ACTION_NO_ACTIVE_SUBTRACKS_LEFT_IN_THE_MAINTRACK = "noActiveSubtracksLeftInMainTrack";
public static final String STREAMS = "streams";
public static final String DEFAULT_LOCALHOST = "127.0.0.1";
protected static Logger logger = LoggerFactory.getLogger(AntMediaApplicationAdapter.class);
private ServerSettings serverSettings;
public static final String VOD = "VoD";
public static final String LIVE_STREAM = "liveStream";
public static final String IP_CAMERA = "ipCamera";
public static final String STREAM_SOURCE = "streamSource";
public static final String PLAY_LIST = "playlist";
protected static final int END_POINT_LIMIT = 20;
//Allow any sub directory under /
private static final String VOD_IMPORT_ALLOWED_DIRECTORY = "/";
private List streamPublishSecurityList;
private Map onvifCameraList = new ConcurrentHashMap<>();
protected StreamFetcherManager streamFetcherManager;
protected Map muxAdaptors = new ConcurrentHashMap<>();
private DataStore dataStore;
private DataStoreFactory dataStoreFactory;
private StreamAcceptFilter streamAcceptFilter;
private AppSettings appSettings;
private Vertx vertx;
protected List encoderBlockedStreams = new ArrayList<>();
private int numberOfEncoderNotOpenedErrors = 0;
protected int publishTimeoutStreams = 0;
private List publishTimeoutStreamsList = new ArrayList<>();
private boolean shutdownProperly = true;
protected WebRTCVideoReceiveStats webRTCVideoReceiveStats = new WebRTCVideoReceiveStats();
protected WebRTCAudioReceiveStats webRTCAudioReceiveStats = new WebRTCAudioReceiveStats();
protected WebRTCVideoSendStats webRTCVideoSendStats = new WebRTCVideoSendStats();
protected WebRTCAudioSendStats webRTCAudioSendStats = new WebRTCAudioSendStats();
private IClusterNotifier clusterNotifier;
protected boolean serverShuttingDown = false;
protected StorageClient storageClient;
protected Queue streamListeners = new ConcurrentLinkedQueue<>();
protected Map playListSchedulerTimer = new ConcurrentHashMap<>();
IClusterStreamFetcher clusterStreamFetcher;
protected ISubtrackPoller subtrackPoller;
private Random random = new Random();
@Override
public boolean appStart(IScope app) {
setScope(app);
for (IStreamPublishSecurity streamPublishSecurity : getStreamPublishSecurityList()) {
registerStreamPublishSecurity(streamPublishSecurity);
}
//init vertx
getVertx();
//initalize to access the data store directly in the code
getDataStore();
// Create initialized file in application
Result result = createInitializationProcess(app.getName());
//initialize storage client
storageClient = (StorageClient) app.getContext().getBean(StorageClient.BEAN_NAME);
if (!result.isSuccess()) {
//Save App Setting
this.shutdownProperly = false;
// Reset Broadcast Stats
resetBroadcasts();
}
if (app.getContext().hasBean(IClusterNotifier.BEAN_NAME)) {
//which means it's in cluster mode
clusterNotifier = (IClusterNotifier) app.getContext().getBean(IClusterNotifier.BEAN_NAME);
logger.info("Registering settings listener to the cluster notifier for app: {}", app.getName());
clusterNotifier.registerSettingUpdateListener(getAppSettings().getAppName(), new IAppSettingsUpdateListener() {
@Override
public boolean settingsUpdated(AppSettings settings) {
return updateSettings(settings, false, true);
}
@Override
public AppSettings getCurrentSettings() {
return getAppSettings();
}
});
AppSettings storedSettings = clusterNotifier.getClusterStore().getSettings(app.getName());
boolean updateClusterSettings = false;
if(storedSettings == null)
{
logger.warn("There is not a stored settings for the app:{}. It will update the database for app settings", app.getName());
storedSettings = appSettings;
updateClusterSettings = true;
}
else if (getServerSettings().getHostAddress().equals(storedSettings.getWarFileOriginServerAddress())
&& storedSettings.isPullWarFile())
{
logger.info("This instance is the host of the app:{} to be deployed to the cluster", app.getName());
//get the current value of isPullWarFile here otherwise it will be set to false below
boolean isPullWarFile = storedSettings.isPullWarFile();
storedSettings = appSettings;
updateClusterSettings = true;
//keep the settings to let the app distributed to all nodes
storedSettings.setPullWarFile(isPullWarFile);
storedSettings.setWarFileOriginServerAddress(getServerSettings().getHostAddress());
}
logger.info("Updating settings while app({}) is being started. AppSettings will be saved to Cluster db? Answer -> {}", app.getName(), updateClusterSettings ? "yes" : "no");
updateSettings(storedSettings, updateClusterSettings, false);
}
vertx.setTimer(1000, l -> {
getStreamFetcherManager();
if(appSettings.isStartStreamFetcherAutomatically()) {
List streams = getDataStore().getExternalStreamsList();
logger.info("Stream source size: {}", streams.size());
for (Broadcast broadcast : streams)
{
if (!broadcast.isAutoStartStopEnabled()) {
//start streaming is auto/stop is not enabled
streamFetcherManager.startStreaming(broadcast);
}
}
}
//Schedule Playlist items
int offset = 0;
int batch = 50;
List playlist;
long now = System.currentTimeMillis();
while ((playlist = getDataStore().getBroadcastList(offset, batch, AntMediaApplicationAdapter.PLAY_LIST, null, null, null)) != null ) {
if (playlist.isEmpty()) {
break;
}
for (Broadcast broadcast : playlist)
{
schedulePlayList(now, broadcast);
}
offset += batch;
}
synchUserVoDFolder(null, appSettings.getVodFolder());
});
AMSShutdownManager.getInstance().subscribe(this);
//With the common app structure, we won't need to null check for WebRTCAdaptor
if (app.getContext().hasBean(IWebRTCAdaptor.BEAN_NAME))
{
IWebRTCAdaptor webRTCAdaptor = (IWebRTCAdaptor) app.getContext().getBean(IWebRTCAdaptor.BEAN_NAME);
webRTCAdaptor.setExcessiveBandwidthValue(appSettings.getExcessiveBandwidthValue());
webRTCAdaptor.setExcessiveBandwidthCallThreshold(appSettings.getExcessiveBandwidthCallThreshold());
webRTCAdaptor.setTryCountBeforeSwitchback(appSettings.getExcessiveBandwithTryCountBeforeSwitchback());
webRTCAdaptor.setExcessiveBandwidthAlgorithmEnabled(appSettings.isExcessiveBandwidthAlgorithmEnabled());
webRTCAdaptor.setPacketLossDiffThresholdForSwitchback(appSettings.getPacketLossDiffThresholdForSwitchback());
webRTCAdaptor.setRttMeasurementDiffThresholdForSwitchback(appSettings.getRttMeasurementDiffThresholdForSwitchback());
}
setStorageclientSettings(appSettings);
logger.info("{} started", app.getName());
return true;
}
public void schedulePlayList(long now, Broadcast broadcast)
{
if (broadcast.getPlannedStartDate() != 0)
{
long startTimeDelay = (broadcast.getPlannedStartDate()*1000) - now;
if (startTimeDelay > 0)
{
//Create some random value to not let any other node pull the stream at the same time.
//I also improve the StreamFetcher to not get started in the WorkerThread if another node is pulling.
//TBH, It's not a good solution and I could not find something better for now
//@mekya
long randomDelay = random.nextInt(5000);
logger.info("Scheduling playlist to play after {}ms with random delay:{}ms, total delay:{}ms for id:{}", startTimeDelay, randomDelay, (startTimeDelay + randomDelay), broadcast.getStreamId());
startTimeDelay += randomDelay;
long timerId = vertx.setTimer(startTimeDelay,
(timer) ->
{
Broadcast freshBroadcast = getDataStore().get(broadcast.getStreamId());
if (freshBroadcast != null &&
AntMediaApplicationAdapter.PLAY_LIST.equals(freshBroadcast.getType()))
{
logger.info("Starting scheduled playlist for id:{} ", freshBroadcast.getStreamId());
streamFetcherManager.startPlaylist(freshBroadcast);
}
else
{
if (freshBroadcast == null) {
logger.warn("Not starting playlist because it's null for stream id:{}. It must have been deleted", broadcast.getStreamId());
}
else {
logger.error("Not starting playlist because wrong configuration for streamId:{}. It should be a bug in somewhere", broadcast.getStreamId());
}
}
playListSchedulerTimer.remove(freshBroadcast.getStreamId());
});
playListSchedulerTimer.put(broadcast.getStreamId(), timerId);
}
}
}
public void cancelPlaylistSchedule(String streamId) {
Long timerId = playListSchedulerTimer.remove(streamId);
if (timerId != null) {
vertx.cancelTimer(timerId);
}
}
/**
* This method is called after ungraceful shutdown
* @return
*/
public Result resetBroadcasts(){
logger.info("Resetting streams viewer numbers because there is an unexpected stop happened in app: {}", getScope() != null? getScope().getName() : "[scope is null]");
int operationCount = getDataStore().resetBroadcasts(getServerSettings().getHostAddress());
logger.info("Resetting subscriber connection status" );
getDataStore().resetSubscribersConnectedStatus();
Result result = new Result(true);
result.setMessage("Successfull operations: "+ operationCount);
return result;
}
/**
* @Deprecated
* This method is deprecated. Use {@link #importVoDFolder(String)} {@link #unlinksVoD(String)}
* @param oldFolderPath
* @param vodFolderPath
* @return
*/
public boolean synchUserVoDFolder(String oldFolderPath, String vodFolderPath)
{
boolean result = false;
File streamsFolder = new File(WEBAPPS_PATH + getScope().getName() + "/streams");
if(oldFolderPath != null && !oldFolderPath.equals("")){
deleteSymbolicLink(new File(oldFolderPath), streamsFolder);
}
if(vodFolderPath != null && !vodFolderPath.equals(""))
{
File f = new File(vodFolderPath);
createSymbolicLink(streamsFolder, f);
//if file does not exists, it means reset the vod
getDataStore().fetchUserVodList(f);
result = true;
}
return result;
}
public Result createSymbolicLink(File streamsFolder, File vodFolder) {
Result result = null;
try {
if (!streamsFolder.exists()) {
streamsFolder.mkdirs();
}
if (vodFolder.exists() && vodFolder.isDirectory())
{
File newLinkFile = new File(streamsFolder, vodFolder.getName());
if (!Files.isSymbolicLink(newLinkFile.toPath()))
{
Path target = vodFolder.toPath();
Files.createSymbolicLink(newLinkFile.toPath(), target);
result = new Result(true);
}
else {
result = new Result(false, "There is already a file with the name "+ vodFolder.getName()+" in the streams directory");
}
}
else {
result = new Result(false, vodFolder.getAbsolutePath() + " does not exist or is not a directory");
}
} catch (IOException e) {
logger.error(ExceptionUtils.getStackTrace(e));
result = new Result(false, "Exception in creating symbolic link");
}
return result;
}
/**
* Import vod files recursively in the directory. It also created symbolic link to make the files streamable
* @param vodFolderPath absolute path of the vod folder to be imported
* @return
*/
public Result importVoDFolder(String vodFolderPath) {
File streamsFolder = new File(WEBAPPS_PATH + getScope().getName() + "/streams");
File directory = new File(vodFolderPath == null ? "" : vodFolderPath);
File allowedDirectory = new File(VOD_IMPORT_ALLOWED_DIRECTORY);
Result result = null;
try {
if (FileUtils.directoryContains(allowedDirectory, directory))
{
result = createSymbolicLink(streamsFolder, directory);
if (result.isSuccess()) {
int numberOfFilesImported = importToDB(directory, directory);
result.setMessage(numberOfFilesImported + " files are imported");
}
}
else {
result = new Result(false, "VoD import directory is allowed under " + VOD_IMPORT_ALLOWED_DIRECTORY );
}
} catch (IOException e) {
logger.error(ExceptionUtils.getStackTrace(e));
result = new Result(false, "VoD import directory is allowed under " + VOD_IMPORT_ALLOWED_DIRECTORY );
}
return result;
}
public Result unlinksVoD(String directory)
{
//check the directory exist
File folder = new File(directory == null ? "" : directory);
Result result = null;
if (folder.exists() && folder.isDirectory()) {
File streamsFolder = new File(WEBAPPS_PATH + getScope().getName() + "/streams");
//check the symbolic links exists and delete it
deleteSymbolicLink(folder, streamsFolder);
int deletedRecords = deleteUserVoDByStreamId(folder.getName());
result = new Result(true, deletedRecords + " of records are deleted");
}
else {
result = new Result(false, directory + " does not exist or it's not a directory");
}
return result;
}
private int deleteUserVoDByStreamId(String streamId)
{
int numberOfDeletedRecords = 0;
List vodList;
do {
vodList = getDataStore().getVodList(0, 50, null, null, streamId, null);
if (vodList != null && !vodList.isEmpty())
{
for (VoD voD : vodList) {
if (VoD.USER_VOD.equals(voD.getType()))
{
if (getDataStore().deleteVod(voD.getVodId())) {
numberOfDeletedRecords++;
}
}
}
}
} while(vodList != null && !vodList.isEmpty());
return numberOfDeletedRecords;
}
public int importToDB(File subDirectory, File baseDirectory)
{
File[] listOfFiles = subDirectory.listFiles();
int numberOfFilesImported = 0;
if (listOfFiles != null)
{
for (File file : listOfFiles) {
String fileExtension = FilenameUtils.getExtension(file.getName());
if (file.isFile() && ("mp4".equals(fileExtension) || "flv".equals(fileExtension)
|| "mkv".equals(fileExtension) || "m3u8".equals(fileExtension)))
{
long fileSize = file.length();
long unixTime = System.currentTimeMillis();
String relativePath = "streams" + File.separator +
subDirectory.getAbsolutePath().substring(baseDirectory.getAbsolutePath().length() - baseDirectory.getName().length())
+ File.separator + file.getName();
String vodId = RandomStringUtils.randomNumeric(24);
//add base directory folder name as streamId in order to find it easily
VoD newVod = new VoD(baseDirectory.getName(), baseDirectory.getName(), relativePath, file.getName(), unixTime, 0, Muxer.getDurationInMs(file, null),
fileSize, VoD.USER_VOD, vodId, null);
if (getDataStore().addVod(newVod) != null)
{
numberOfFilesImported++;
}
}
else if (file.isDirectory())
{
numberOfFilesImported += importToDB(file, baseDirectory);
}
}
}
return numberOfFilesImported;
}
/**
* Deletes the symbolic link under the streams directory
* @param vodDirectory
* @param streamsFolder
* @return
* @throws IOException
*/
public boolean deleteSymbolicLink(File vodDirectory, File streamsFolder){
boolean result = false;
try {
if (vodDirectory != null && streamsFolder != null)
{
File linkFile = new File(streamsFolder.getAbsolutePath(), vodDirectory.getName());
if (!streamsFolder.getAbsolutePath().equals(linkFile.getAbsolutePath())
&&
Files.isSymbolicLink(linkFile.toPath()))
{
Files.delete(linkFile.toPath());
result = true;
}
}
} catch (IOException e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
return result;
}
public String getListenerHookURL(Broadcast broadcast)
{
String listenerHookURL = broadcast != null ? broadcast.getListenerHookURL() : null;
if (StringUtils.isBlank(listenerHookURL))
{
listenerHookURL = getAppSettings().getListenerHookURL();
}
return listenerHookURL;
}
public void closeBroadcast(String streamId) {
try {
logger.info("Closing broadcast stream id: {}", streamId);
Broadcast broadcast = getDataStore().get(streamId);
if (broadcast != null) {
if (broadcast.isZombi()) {
logger.info("Deleting streamId:{} because it's a zombi stream", streamId);
getDataStore().delete(streamId);
}
else {
getDataStore().updateStatus(streamId, BROADCAST_STATUS_FINISHED);
// This is resets Viewer map in HLS Viewer Stats
resetHLSStats(streamId);
// This is resets Viewer map in DASH Viewer Stats
resetDASHStats(streamId);
}
final String listenerHookURL = getListenerHookURL(broadcast);
if (listenerHookURL != null && !listenerHookURL.isEmpty()) {
final String name = broadcast.getName();
final String category = broadcast.getCategory();
final String metaData = broadcast.getMetaData();
final String mainTrackId = broadcast.getMainTrackStreamId();
logger.info("call live stream ended hook for stream:{}",streamId );
notifyHook(listenerHookURL, streamId, mainTrackId, HOOK_ACTION_END_LIVE_STREAM, name, category,
null, null, metaData, null);
}
PublishEndedEvent publishEndedEvent = new PublishEndedEvent();
publishEndedEvent.setStreamId(streamId);
publishEndedEvent.setDurationMs(System.currentTimeMillis() - broadcast.getStartTime());
publishEndedEvent.setApp(scope.getName());
LoggerUtils.logAnalyticsFromServer(publishEndedEvent);
if(StringUtils.isNotBlank(broadcast.getMainTrackStreamId())) {
updateMainTrackWithRecentlyFinishedBroadcast(broadcast);
}
for (IStreamListener listener : streamListeners) {
listener.streamFinished(broadcast.getStreamId());
}
logger.info("Leaving closeBroadcast for streamId:{}", streamId);
}
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
}
public static Broadcast saveMainBroadcast(String streamId, String mainTrackId, DataStore dataStore) {
Broadcast mainBroadcast = new Broadcast();
try {
mainBroadcast.setStreamId(mainTrackId);
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
mainBroadcast.setZombi(true);
mainBroadcast.setStatus(BROADCAST_STATUS_BROADCASTING);
mainBroadcast.getSubTrackStreamIds().add(streamId);
// don't set setOriginAdress because it's not a real stream and it causes extra delay -> mainBroadcast.setOriginAdress(serverSettings.getHostAddress())
mainBroadcast.setStartTime(System.currentTimeMillis());
return StringUtils.isNotBlank(dataStore.save(mainBroadcast)) ? mainBroadcast : null;
}
public static boolean isInstanceAlive(String originAdress, String hostAddress, int httpPort, String appName) {
if (StringUtils.isBlank(originAdress) || StringUtils.equals(originAdress, hostAddress)) {
return true;
}
String url = "http://" + originAdress + ":" + httpPort + "/" + appName;
boolean result = isEndpointReachable(url);
if (!result) {
logger.warn("Instance with origin address {} is not reachable through its app:{}", originAdress, appName);
}
return result;
}
public static boolean isEndpointReachable(String endpoint) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.method("HEAD", HttpRequest.BodyPublishers.noBody()) // HEAD request
.timeout(java.time.Duration.ofSeconds(1))
.build();
try {
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding());
} catch (InterruptedException e) {
logger.error("InterruptedException Enpoint is not reachable: {}, {}", endpoint, ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
logger.error("Enpoint is not reachable: {}, {}", endpoint, ExceptionUtils.getStackTrace(e));
return false;
}
return true;
}
/**
* If multiple threads enter the method at the same time, the following method does not work correctly.
* So we have made it synchronized
*
* It fixes the bug that sometimes main track(room) is not deleted in the video conferences
*
* mekya
*
* @param finishedBroadcast
*/
public synchronized void updateMainTrackWithRecentlyFinishedBroadcast(Broadcast finishedBroadcast)
{
Broadcast mainBroadcast = getDataStore().get(finishedBroadcast.getMainTrackStreamId());
logger.info("updating main track:{} status with recently finished broadcast:{}", finishedBroadcast.getMainTrackStreamId(), finishedBroadcast.getStreamId());
if (mainBroadcast != null) {
mainBroadcast.getSubTrackStreamIds().remove(finishedBroadcast.getStreamId());
long activeSubtracksCount = getDataStore().getActiveSubtracksCount(mainBroadcast.getStreamId(), null);
if (activeSubtracksCount == 0) {
if (mainBroadcast.isZombi()) {
logger.info("Deleting main track streamId:{} because it's a zombi stream and there is no activeSubtrack", mainBroadcast.getStreamId());
getDataStore().delete(mainBroadcast.getStreamId());
}
else {
logger.info("Update main track:{} status to finished ", finishedBroadcast.getMainTrackStreamId());
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
getDataStore().updateBroadcastFields(mainBroadcast.getStreamId(), broadcastUpdate);
}
notifyNoActiveSubtracksLeftInMainTrack(mainBroadcast);
}
else {
logger.info("There are {} active subtracks in the main track:{} status to finished. Just removing the subtrack:{}", activeSubtracksCount, finishedBroadcast.getMainTrackStreamId(), finishedBroadcast.getStreamId());
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setSubTrackStreamIds(mainBroadcast.getSubTrackStreamIds());
getDataStore().updateBroadcastFields(mainBroadcast.getStreamId(), broadcastUpdate);
}
}
else {
logger.warn("Maintrack is null while removing subtrack from maintrack for streamId:{} maintrackId:{}", finishedBroadcast.getStreamId(), finishedBroadcast.getMainTrackStreamId());
}
leftTheRoom(finishedBroadcast.getMainTrackStreamId(), finishedBroadcast.getStreamId());
}
public void resetHLSStats(String streamId) {
if (scope.getContext().getApplicationContext().containsBean(HlsViewerStats.BEAN_NAME)) {
HlsViewerStats hlsViewerStats = (HlsViewerStats) scope.getContext().getApplicationContext().getBean(HlsViewerStats.BEAN_NAME);
hlsViewerStats.resetViewerMap(streamId, ViewerStats.HLS_TYPE);
}
}
public void resetDASHStats(String streamId) {
if (scope.getContext().getApplicationContext().containsBean(DashViewerStats.BEAN_NAME)) {
DashViewerStats dashViewerStats = (DashViewerStats) scope.getContext().getApplicationContext().getBean(DashViewerStats.BEAN_NAME);
dashViewerStats.resetViewerMap(streamId, ViewerStats.DASH_TYPE);
}
}
@Override
public void streamPlayItemPlay(ISubscriberStream stream, IPlayItem item, boolean isLive) {
vertx.setTimer(1, l -> getDataStore().updateRtmpViewerCount(item.getName(), true));
}
@Override
public void streamPlayItemStop(ISubscriberStream stream, IPlayItem item) {
vertx.setTimer(1, l -> getDataStore().updateRtmpViewerCount(item.getName(), false));
}
@Override
public void streamSubscriberClose(ISubscriberStream stream) {
vertx.setTimer(1, l -> getDataStore().updateRtmpViewerCount(stream.getBroadcastStreamPublishName(), false));
}
@Override
public void startPublish(String streamId, long absoluteStartTimeMs, String publishType) {
vertx.executeBlocking( () -> {
try {
Broadcast broadcast = updateBroadcastStatus(streamId, absoluteStartTimeMs, publishType, getDataStore().get(streamId));
final String listenerHookURL = getListenerHookURL(broadcast);
if (listenerHookURL != null && !listenerHookURL.isEmpty())
{
final String name = broadcast.getName();
final String category = broadcast.getCategory();
final String metaData = broadcast.getMetaData();
final String mainTrackId = broadcast.getMainTrackStreamId();
logger.info("Call live stream started hook for stream:{}",streamId );
notifyHook(listenerHookURL, streamId, mainTrackId, HOOK_ACTION_START_LIVE_STREAM, name, category,
null, null, metaData, null);
}
int ingestingStreamLimit = appSettings.getIngestingStreamLimit();
long activeBroadcastNumber = dataStore.getActiveBroadcastCount();
if (ingestingStreamLimit != -1 && activeBroadcastNumber > ingestingStreamLimit)
{
logger.info("Active broadcast count({}) is more than ingesting stream limit:{} so stopping broadcast:{}", activeBroadcastNumber, ingestingStreamLimit, broadcast.getStreamId());
stopStreaming(broadcast);
}
for (IStreamListener listener : streamListeners) {
listener.streamStarted(broadcast.getStreamId());
}
long videoHeight = 0;
long videoWidth = 0;
String videoCodecName=null;
String audioCodecName=null;
MuxAdaptor adaptor = getMuxAdaptor(streamId);
if(adaptor!=null) {
if(adaptor.isEnableVideo()) {
AVCodecParameters videoCodecPar = adaptor.getVideoCodecParameters();
videoWidth = videoCodecPar.width();
videoHeight = videoCodecPar.height();
videoCodecName = avcodec_get_name(videoCodecPar.codec_id()).getString();
}
if(adaptor.isEnableAudio()) {
audioCodecName = avcodec_get_name(adaptor.getAudioCodecParameters().codec_id()).getString();
}
}
PublishStartedEvent event = new PublishStartedEvent();
event.setStreamId(streamId);
event.setProtocol(publishType);
event.setHeight((int) videoHeight);
event.setWidth((int) videoWidth);
event.setVideoCodec(videoCodecName);
event.setAudioCodec(audioCodecName);
event.setApp(scope.getName());
LoggerUtils.logAnalyticsFromServer(event);
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
return null;
});
if (absoluteStartTimeMs == 0)
{
vertx.setTimer(2000, h ->
{
IBroadcastStream broadcastStream = getBroadcastStream(getScope(), streamId);
if (broadcastStream instanceof ClientBroadcastStream)
{
long absoluteStarTime = ((ClientBroadcastStream)broadcastStream).getAbsoluteStartTimeMs();
if (absoluteStarTime != 0)
{
Broadcast broadcast = getDataStore().get(streamId);
if (broadcast != null)
{
broadcast.setAbsoluteStartTimeMs(absoluteStarTime);
getDataStore().save(broadcast);
logger.info("Updating broadcast absolute time {} ms for stream:{}", absoluteStarTime, streamId);
}
else {
logger.info("Broadcast is not available in the database to update the absolute start time for stream:{}", streamId);
}
}
else {
logger.info("Broadcast absolute time is not available for stream:{}", streamId);
}
}
});
}
logger.info("start publish leaved for stream:{}", streamId);
}
public Broadcast updateBroadcastStatus(String streamId, long absoluteStartTimeMs, String publishType, Broadcast broadcast) {
return updateBroadcastStatus(streamId, absoluteStartTimeMs, publishType, broadcast, IAntMediaStreamHandler.BROADCAST_STATUS_BROADCASTING);
}
public Broadcast updateBroadcastStatus(String streamId, long absoluteStartTimeMs, String publishType, Broadcast broadcast, String status) {
if (broadcast == null)
{
logger.info("Saving zombi broadast to data store with streamId:{}", streamId);
broadcast = saveUndefinedBroadcast(streamId, null, this, status, absoluteStartTimeMs, publishType, "", "", "");
}
else {
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setStatus(status);
long now = System.currentTimeMillis();
broadcastUpdate.setStartTime(now);
broadcastUpdate.setUpdateTime(now);
broadcastUpdate.setOriginAdress(getServerSettings().getHostAddress());
broadcastUpdate.setWebRTCViewerCount(0);
broadcastUpdate.setHlsViewerCount(0);
broadcastUpdate.setDashViewerCount(0);
broadcastUpdate.setPublishType(publishType);
//updateBroadcastFields just updates broadcast with the updated fields. No need to give real object
boolean result = getDataStore().updateBroadcastFields(broadcast.getStreamId(), broadcastUpdate);
logger.info(" Status of stream {} is set to {} with result: {}", broadcast.getStreamId(), status, result);
}
return broadcast;
}
public ServerSettings getServerSettings()
{
if (serverSettings == null) {
serverSettings = (ServerSettings)scope.getContext().getApplicationContext().getBean(ServerSettings.BEAN_NAME);
}
return serverSettings;
}
public static Broadcast saveUndefinedBroadcast(String streamId, String streamName, AntMediaApplicationAdapter appAdapter, String streamStatus, long absoluteStartTimeMs, String publishType, String mainTrackStreamId, String metaData, String role) {
Broadcast newBroadcast = new Broadcast();
long now = System.currentTimeMillis();
newBroadcast.setDate(now);
newBroadcast.setStartTime(now);
newBroadcast.setUpdateTime(now);
newBroadcast.setZombi(true);
newBroadcast.setName(streamName);
newBroadcast.setMainTrackStreamId(mainTrackStreamId);
newBroadcast.setMetaData(metaData);
newBroadcast.setRole(role);
try {
newBroadcast.setStreamId(streamId);
newBroadcast.setPublishType(publishType);
return RestServiceBase.saveBroadcast(newBroadcast,
streamStatus, appAdapter.getScope().getName(), appAdapter.getDataStore(),
appAdapter.getAppSettings().getListenerHookURL(), appAdapter.getServerSettings(), absoluteStartTimeMs);
} catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
return null;
}
@Override
public void muxingFinished(final String streamId, File file, long startTime, long duration, int resolution, String previewFilePath, String vodId) {
String vodName = file.getName();
String filePath = file.getPath();
long fileSize = file.length();
long systemTime = System.currentTimeMillis();
String relativePath = getRelativePath(filePath);
String listenerHookURL = null;
String streamName = file.getName();
Broadcast broadcast = getDataStore().get(streamId);
if(broadcast != null){
listenerHookURL = broadcast.getListenerHookURL();
if(broadcast.getName() != null){
streamName = resolution != 0 ? broadcast.getName() + " (" + resolution + "p)" : broadcast.getName();
}
}
//We need to get the webhook url explicitly because broadcast may be deleted here
if (listenerHookURL == null || listenerHookURL.isEmpty()) {
// if hook URL is not defined for stream specific, then try to get common one from app
listenerHookURL = appSettings.getListenerHookURL();
}
String vodIdFinal;
if (vodId != null) {
vodIdFinal = vodId;
}
else {
vodIdFinal = RandomStringUtils.randomAlphanumeric(24);
}
VoD newVod = new VoD(streamName, streamId, relativePath, vodName, systemTime, startTime, duration, fileSize, VoD.STREAM_VOD, vodIdFinal, previewFilePath);
if(broadcast != null){
newVod.setDescription(broadcast.getDescription());
newVod.setMetadata(broadcast.getMetaData());
newVod.setLongitude(broadcast.getLongitude());
newVod.setLatitude(broadcast.getLatitude());
newVod.setAltitude(broadcast.getAltitude());
}
if (getDataStore().addVod(newVod) == null) {
logger.warn("Stream vod with stream id {} cannot be added to data store", streamId);
}
int index;
//HOOK_ACTION_VOD_READY is called only the listenerHookURL is defined either for stream or in AppSettings
if (StringUtils.isNotBlank(listenerHookURL) &&
((index = vodName.lastIndexOf(".mp4")) != -1)
|| ((index = vodName.lastIndexOf(".webm")) != -1) )
{
final String baseName = vodName.substring(0, index);
final String metaData = (broadcast != null) ? broadcast.getMetaData() : null;
logger.info("Setting timer for calling vod ready hook for stream:{}", streamId);
notifyHook(listenerHookURL, streamId, null, HOOK_ACTION_VOD_READY, null, null, baseName, vodIdFinal, metaData, null);
}
String muxerFinishScript = appSettings.getMuxerFinishScript();
if (muxerFinishScript != null && !muxerFinishScript.isEmpty()) {
runScript(muxerFinishScript + " " + file.getAbsolutePath());
}
}
public void notifyFirstActiveSubtrackInMainTrack(Broadcast mainTrack, String subtrackId)
{
final String listenerHookURL = getListenerHookURL(mainTrack);
if(listenerHookURL == null || listenerHookURL.isEmpty()){
return;
}
final String name = mainTrack.getName();
final String category = mainTrack.getCategory();
notifyHook(listenerHookURL, subtrackId, mainTrack.getStreamId(), HOOK_ACTION_FIRST_ACTIVE_SUBTRACK_ADDED_IN_THE_MAINTRACK, name, category, null, null, null, null);
}
public void notifyNoActiveSubtracksLeftInMainTrack(Broadcast mainTrack)
{
final String listenerHookURL = getListenerHookURL(mainTrack);
if (StringUtils.isNotBlank(listenerHookURL)) {
final String name = mainTrack.getName();
final String category = mainTrack.getCategory();
notifyHook(listenerHookURL, mainTrack.getStreamId(), null, HOOK_ACTION_NO_ACTIVE_SUBTRACKS_LEFT_IN_THE_MAINTRACK, name, category, null, null, null, null);
}
}
public void runScript(String scriptFile) {
vertx.executeBlocking(() -> {
try {
logger.info("running script: {}", scriptFile);
Process exec = Runtime.getRuntime().exec(scriptFile);
int result = exec.waitFor();
logger.info("completing script: {} with return value {}", scriptFile, result);
} catch (IOException e) {
logger.error(ExceptionUtils.getStackTrace(e));
} catch (InterruptedException e) {
logger.error(ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
}
return null;
}, false);
}
public static String getRelativePath(String filePath){
StringBuilder relativePath= new StringBuilder();
String[] subDirs = filePath.split(STREAMS);
if(subDirs.length == 2)
relativePath = new StringBuilder(STREAMS + subDirs[1]);
else{
for(int i=1;i {
logger.info("Running notify hook url:{} stream id: {} mainTrackId:{} action:{} vod name:{} vod id:{}", url, id, mainTrackId, action, vodName, vodId);
Map variables = new HashMap<>();
variables.put("id", id);
variables.put("action", action);
putToMap("streamName", streamName, variables);
putToMap("category", category, variables);
putToMap("vodName", vodName, variables);
putToMap("vodId", vodId, variables);
putToMap("mainTrackId", mainTrackId, variables);
putToMap("roomId", mainTrackId, variables);
putToMap("subscriberId", subscriberId, variables);
if (StringUtils.isNotBlank(metadata)) {
Object metaDataJsonObj = null;
try {
JSONParser jsonParser = new JSONParser();
metaDataJsonObj = (JSONObject) jsonParser.parse(metadata);
} catch (ParseException parseException) {
metaDataJsonObj = metadata;
}
putToMap("metadata", metaDataJsonObj, variables);
}
putToMap("timestamp", String.valueOf(System.currentTimeMillis()), variables);
try {
sendPOST(url, variables, appSettings.getWebhookRetryCount(), appSettings.getWebhookContentType());
} catch (Exception exception) {
logger.error(ExceptionUtils.getStackTrace(exception));
}
return null;
}, false);
}
@Override
public void notifyWebhookForStreamStatus(Broadcast broadcast, int width, int height, long totalByteReceived,
int inputQueueSize, double speed) {
String listenerHookURL = getListenerHookURL(broadcast);
if (StringUtils.isNotBlank(listenerHookURL)) {
vertx.executeBlocking(() ->
{
Map variables = new HashMap<>();
variables.put("id", broadcast.getStreamId());
variables.put("action", HOOK_ACTION_STREAM_STATUS);
variables.put("width", width);
variables.put("height", height);
variables.put("totalByteReceived", totalByteReceived);
variables.put("inputQueueSize", inputQueueSize);
variables.put("speed", speed);
variables.put("timestamp", System.currentTimeMillis());
variables.put("streamName",broadcast.getName());
try {
sendPOST(listenerHookURL, variables, appSettings.getWebhookRetryCount(), appSettings.getWebhookContentType());
} catch (Exception exception) {
logger.error(ExceptionUtils.getStackTrace(exception));
}
return null;
}, false);
}
}
private void putToMap(String keyName, Object keyValue, Map map) {
if (keyValue != null && StringUtils.isNotBlank(keyValue.toString())) {
map.put(keyName, keyValue);
}
}
/**
*
* @param url
* @param variables
* @param retryAttempts
* @param sendType the type of the entity to be sent. It can be either "application/x-www-form-urlencoded" or "application/json"
*/
public void sendPOST(String url, Map variables, int retryAttempts, String contentType) {
logger.info("Sending POST request to {}", url);
try (CloseableHttpClient httpClient = getHttpClient()) {
HttpPost httpPost = new HttpPost(url);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(2000)
.setConnectionRequestTimeout(2000)
.setSocketTimeout(2000)
.build();
httpPost.setConfig(requestConfig);
if (ContentType.APPLICATION_FORM_URLENCODED.getMimeType().equals(contentType))
{
List urlParameters = new ArrayList<>();
Set> entrySet = variables.entrySet();
for (Entry entry : entrySet)
{
urlParameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
}
HttpEntity postParams = new UrlEncodedFormEntity(urlParameters);
httpPost.setEntity(postParams);
}
else
{
JSONObject hookPayload = new JSONObject(variables);
httpPost.setEntity(new StringEntity(hookPayload.toString(), ContentType.APPLICATION_JSON));
}
try (CloseableHttpResponse httpResponse = httpClient.execute(httpPost)) {
int statusCode = httpResponse.getStatusLine().getStatusCode();
logger.info("POST Response Status: {}", statusCode);
if (statusCode != HttpStatus.SC_OK)
{
if (retryAttempts >= 1)
{
logger.info("Retry attempt for POST in {} milliseconds due to non-200 response: {}", appSettings.getWebhookRetryDelay(), statusCode);
retrySendPostWithDelay(url, variables, retryAttempts - 1, contentType);
} else if (appSettings.getWebhookRetryCount() != 0)
{
logger.info("Stopping sending POST because no more retry attempts left. Giving up.");
}
}
}
} catch (IOException e) {
if (retryAttempts >= 1)
{
logger.info("Retry attempt for POST in {} milliseconds due to IO exception: {}", appSettings.getWebhookRetryDelay(), ExceptionUtils.getStackTrace(e));
retrySendPostWithDelay(url, variables, retryAttempts - 1, contentType);
}
else if (appSettings.getWebhookRetryCount() != 0)
{
logger.info("Stopping sending POST because no more retry attempts left. Giving up.");
}
}
}
public void retrySendPostWithDelay(String url, Map variables, int retryAttempts, String contentType) {
vertx.setTimer(appSettings.getWebhookRetryDelay(), timerId -> {
sendPOST(url, variables, retryAttempts, contentType);
});
}
public CloseableHttpClient getHttpClient() {
return HttpClients.createDefault();
}
public List getStreamPublishSecurityList() {
return streamPublishSecurityList;
}
public void setStreamPublishSecurityList(List streamPublishSecurityList) {
this.streamPublishSecurityList = streamPublishSecurityList;
}
@Override
public AppSettings getAppSettings() {
return appSettings;
}
public void setAppSettings(AppSettings appSettings) {
this.appSettings = appSettings;
}
public StreamAcceptFilter getStreamAcceptFilter() {
return streamAcceptFilter;
}
public void setStreamAcceptFilter(StreamAcceptFilter streamAcceptFilter) {
this.streamAcceptFilter = streamAcceptFilter;
}
@Override
public boolean isValidStreamParameters(int width, int height, int fps, int bitrate, String streamId) {
return streamAcceptFilter.isValidStreamParameters(width, height, fps, bitrate, streamId);
}
public static final boolean isStreaming(Broadcast broadcast) {
//if updatetime is older than 2 times update period time, regard that it's not streaming
return System.currentTimeMillis() - broadcast.getUpdateTime() < (2 * MuxAdaptor.STAT_UPDATE_PERIOD_MS) &&
(IAntMediaStreamHandler.BROADCAST_STATUS_BROADCASTING.equals(broadcast.getStatus())
|| IAntMediaStreamHandler.BROADCAST_STATUS_PREPARING.equals(broadcast.getStatus()));
}
public Result startStreaming(Broadcast broadcast)
{
Result result = new Result(false);
if(broadcast.getType().equals(AntMediaApplicationAdapter.IP_CAMERA) ||
broadcast.getType().equals(AntMediaApplicationAdapter.STREAM_SOURCE) ||
broadcast.getType().equals(AntMediaApplicationAdapter.VOD)
) {
result = getStreamFetcherManager().startStreaming(broadcast);
}
else if (broadcast.getType().equals(AntMediaApplicationAdapter.PLAY_LIST)) {
result = getStreamFetcherManager().startPlaylist(broadcast);
}
else {
logger.info("Broadcast type is not supported for startStreaming:{} streamId:{}", broadcast.getType(), broadcast.getStreamId());
}
return result;
}
public Result stopStreaming(Broadcast broadcast)
{
Result result = new Result(false);
logger.info("stopStreaming is called for stream:{}", broadcast.getStreamId());
if (broadcast.getType().equals(AntMediaApplicationAdapter.IP_CAMERA) ||
broadcast.getType().equals(AntMediaApplicationAdapter.STREAM_SOURCE) ||
broadcast.getType().equals(AntMediaApplicationAdapter.VOD))
{
result = getStreamFetcherManager().stopStreaming(broadcast.getStreamId());
}
else if (broadcast.getType().equals(AntMediaApplicationAdapter.PLAY_LIST))
{
result = getStreamFetcherManager().stopPlayList(broadcast.getStreamId());
}
else if (broadcast.getType().equals(AntMediaApplicationAdapter.LIVE_STREAM))
{
IBroadcastStream broadcastStream = getBroadcastStream(getScope(), broadcast.getStreamId());
if (broadcastStream != null)
{
IStreamCapableConnection connection = ((IClientBroadcastStream) broadcastStream).getConnection();
if (connection != null) {
connection.close();
}
else {
logger.warn("Connection is null. It should not happen for stream: {}. Analyze the logs", broadcast.getStreamId());
}
result.setSuccess(true);
}
}
return result;
}
public OnvifCamera getOnvifCamera(String id) {
OnvifCamera onvifCamera = onvifCameraList.get(id);
if (onvifCamera == null) {
Broadcast camera = getDataStore().get(id);
if (camera != null) {
onvifCamera = new OnvifCamera();
onvifCamera.connect(camera.getIpAddr(), camera.getUsername(), camera.getPassword());
onvifCameraList.put(id, onvifCamera);
}
}
return onvifCamera;
}
public StreamFetcherManager getStreamFetcherManager() {
if(streamFetcherManager == null) {
streamFetcherManager = new StreamFetcherManager(vertx, getDataStore(), getScope());
}
return streamFetcherManager;
}
public void setStreamFetcherManager(StreamFetcherManager streamFetcherManager) {
this.streamFetcherManager = streamFetcherManager;
}
@Override
public void setQualityParameters(String id, String quality, double speed, int pendingPacketSize, long updateTimeMs) {
vertx.runOnContext(h -> {
Broadcast broadcastLocal = getDataStore().get(id);
if (broadcastLocal != null)
{
//round the number to three decimal places,
double roundedSpeed = Math.round(speed * 1000.0) / 1000.0;
logger.debug("update source quality for stream: {} quality:{} speed:{}", id, quality, speed);
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setSpeed(roundedSpeed);
broadcastUpdate.setPendingPacketSize(pendingPacketSize);
broadcastUpdate.setUpdateTime(updateTimeMs);
broadcastUpdate.setQuality(quality);
long elapsedTime = System.currentTimeMillis() - broadcastLocal.getStartTime();
broadcastUpdate.setDuration(elapsedTime);
getDataStore().updateBroadcastFields(id, broadcastUpdate);
ViewerCountEvent viewerCountEvent = new ViewerCountEvent();
viewerCountEvent.setApp(getScope().getName());
viewerCountEvent.setStreamId(id);
viewerCountEvent.setDashViewerCount(broadcastLocal.getDashViewerCount());
viewerCountEvent.setHlsViewerCount(broadcastLocal.getHlsViewerCount());
viewerCountEvent.setWebRTCViewerCount(broadcastLocal.getWebRTCViewerCount());
LoggerUtils.logAnalyticsFromServer(viewerCountEvent);
}
});
}
@Override
public DataStore getDataStore() {
//vertx should be initialized before calling this method
if(dataStore == null)
{
dataStore = dataStoreFactory.getDataStore();
}
return dataStore;
}
public void setDataStore(DataStore dataStore) {
this.dataStore = dataStore;
}
public DataStoreFactory getDataStoreFactory() {
return dataStoreFactory;
}
public void setDataStoreFactory(DataStoreFactory dataStoreFactory) {
this.dataStoreFactory = dataStoreFactory;
}
/**
* This setter for test cases
* @param vertx
*/
public void setVertx(Vertx vertx) {
this.vertx = vertx;
}
public void closeRTMPStreams()
{
Collection adaptors = getMuxAdaptors();
synchronized (adaptors)
{
for (MuxAdaptor adaptor : adaptors) {
if(adaptor.getBroadcast().getType().equals(AntMediaApplicationAdapter.LIVE_STREAM)) {
ClientBroadcastStream broadcastStream = adaptor.getBroadcastStream();
if (broadcastStream != null) {
broadcastStream.stop();
}
adaptor.stop(true);
}
}
}
}
public void closeStreamFetchers() {
if (streamFetcherManager != null) {
Map fetchers = streamFetcherManager.getStreamFetcherList();
for (StreamFetcher streamFetcher : fetchers.values()) {
streamFetcher.stopStream();
//it may be also play list so stop it if it's
getStreamFetcherManager().stopPlayList(streamFetcher.getStreamId());
}
fetchers.clear();
}
}
public void waitUntilLiveStreamsStopped() {
int i = 0;
int waitPeriod = 500;
boolean everythingHasStopped = true;
while(getDataStore().getLocalLiveBroadcastCount(getServerSettings().getHostAddress()) > 0) {
try {
if (i > 3) {
logger.warn("Waiting for active broadcasts number decrease to zero for app: {}"
+ " total wait time: {}ms", getScope().getName(), i*waitPeriod);
}
if (i>10) {
logger.error("Not all live streams're stopped gracefully. It will update the streams' status to finished explicitly");
everythingHasStopped = false;
break;
}
i++;
Thread.sleep(waitPeriod);
} catch (InterruptedException e) {
logger.error(ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
}
}
if (!everythingHasStopped)
{
List localLiveBroadcasts = getDataStore().getLocalLiveBroadcasts(getServerSettings().getHostAddress());
List streamIdList = new ArrayList<>();
for (Broadcast broadcast : localLiveBroadcasts) {
//if it's not closed properly, let's set the state to failed
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
broadcastUpdate.setPlayListStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
broadcastUpdate.setWebRTCViewerCount(0);
broadcastUpdate.setHlsViewerCount(0);
broadcastUpdate.setDashViewerCount(0);
getDataStore().updateBroadcastFields(broadcast.getStreamId(), broadcastUpdate);
streamIdList.add(broadcast.getStreamId());
}
if (logger.isWarnEnabled()) {
logger.warn("Following streams status set to finished explicitly because they're not stopped properly: {}", String.join(",", streamIdList));
}
}
}
public void waitUntilThreadsStop() {
int i = 0;
int waitPeriod = 1000;
int activeVertxThreadCount = 0;
while((activeVertxThreadCount = getActiveVertxThreadCount()) > 0) {
try {
if (i > 3) {
logger.warn("Waiting for active vertx threads count({}) decrease to zero for app: {}"
+ " total wait time: {}ms", activeVertxThreadCount, getScope().getName(), i*waitPeriod);
}
if (i>10) {
logger.error("*********************************************************************");
logger.error("Not all active vertx threads are stopped. It's even breaking the loop");
logger.error("*********************************************************************");
break;
}
i++;
Thread.sleep(waitPeriod);
} catch (InterruptedException e) {
logger.error(ExceptionUtils.getStackTrace(e));
Thread.currentThread().interrupt();
}
}
}
private int getActiveVertxThreadCount() {
int activeVertexThreadCount = 0;
try {
MetricsService metricsService = MetricsService.create(vertx);
String activeThreadKey = "vertx.pools.worker.vert.x-worker-thread.in-use";
JsonObject metrics = metricsService.getMetricsSnapshot(activeThreadKey);
if (metrics != null) {
activeVertexThreadCount = metrics.getJsonObject(activeThreadKey).getInteger("count");
}
}
catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
return activeVertexThreadCount;
}
@Override
public void serverShuttingdown() {
stopApplication(false);
}
@Override
public void appStop(IScope app) {
super.appStop(app);
//we may use this method for stopApplication
logger.info("appStop is being called for {}", app.getName());
}
public void stopApplication(boolean deleteDB) {
logger.info("{} is closing streams", getScope().getName());
serverShuttingDown = true;
closeStreamFetchers();
closeRTMPStreams();
waitUntilLiveStreamsStopped();
waitUntilThreadsStop();
createShutdownFile(getScope().getName());
closeDB(deleteDB);
}
public void closeDB(boolean deleteDB) {
boolean isClusterMode = getScope().getContext().hasBean(IClusterNotifier.BEAN_NAME);
if (deleteDB && isClusterMode)
{
//let the other nodes have enough time to synch
getVertx().setTimer(ClusterNode.NODE_UPDATE_PERIOD + 1000, l->
getDataStore().close(deleteDB)
);
}
else {
getDataStore().close(deleteDB);
}
}
public Result createInitializationProcess(String appName){
Result result = new Result(false);
String initializedFilePath =WEBAPPS_PATH + appName + "/.initialized";
File initializedFile = new File(initializedFilePath);
String closedFilePath =WEBAPPS_PATH + appName + "/.closed";
File closedFile = new File(closedFilePath);
try {
// Check first start
if(!initializedFile.exists() && !closedFile.exists()) {
createInitializationFile(appName, result, initializedFile);
}
// Check repeated starting - It's normal start
else if(initializedFile.exists() && closedFile.exists()) {
// Delete old .closed file in application
Files.delete(closedFile.toPath());
if(!closedFile.exists()) {
result.setMessage("System works, deleted closed file in " + appName);
result.setSuccess(true);
logger.info("Delete the \".closed\" file in {}",appName);
}
else {
result.setMessage("Delete couldn't closed file in " + appName);
result.setSuccess(false);
logger.info("Not deleted the \".closed\" file in {}",appName);
}
}
// It means didn't close normal (unexpected stop)
else if(initializedFile.exists() && !closedFile.exists()) {
// It's problem here
// Notify user to send logs
result.setMessage("Something wrong in " + appName);
result.setSuccess(false);
logger.error("Something wrong in {}",appName);
}
else {
// Other odd is initialization file doesn't exist and closed file exist.
// This case happens when app is created but not deployed for some reason
createInitializationFile(appName, result, initializedFile);
Files.deleteIfExists(closedFile.toPath());
}
} catch (IOException e) {
logger.error(e.getMessage());
}
return result;
}
public void createInitializationFile(String appName, Result result, File initializedFile) throws IOException {
if(initializedFile.createNewFile()) {
result.setMessage("Initialized file created in " + appName);
result.setSuccess(true);
logger.info("Initialized file is created in {}",appName);
}
else {
result.setMessage("Initialized file couldn't create in " + appName);
result.setSuccess(false);
logger.info("Initialized file couldn't be created in {}",appName);
}
}
public void createShutdownFile(String appName){
String closedFilePath =WEBAPPS_PATH + appName + "/.closed";
File closedFile = new File(closedFilePath);
try {
if(!closedFile.exists()) {
if(closedFile.createNewFile()) {
logger.info("Closed file created in {}",appName);
}
else {
logger.error("Closed file couldn't create in {}",appName);
}
}
else {
logger.warn("Closed file already exists for app: {}", appName);
}
} catch (IOException e) {
logger.error(e.getMessage());
}
}
public boolean isShutdownProperly() {
return shutdownProperly;
}
public void setShutdownProperly(boolean shutdownProperly) {
this.shutdownProperly = shutdownProperly;
}
@Override
public void muxAdaptorAdded(MuxAdaptor muxAdaptor){
muxAdaptors.put(muxAdaptor.getStreamId(), muxAdaptor);
}
@Override
public void muxAdaptorRemoved(MuxAdaptor muxAdaptor) {
muxAdaptors.remove(muxAdaptor.getStreamId());
}
public MuxAdaptor getMuxAdaptor(String streamId) {
return muxAdaptors.get(streamId);
}
public Collection getMuxAdaptors() {
return muxAdaptors.values();
}
/**
* Number of encoders blocked.
* @return
*/
public int getNumberOfEncodersBlocked() {
return encoderBlockedStreams.size();
}
public synchronized void encoderBlocked(String streamId, boolean blocked) {
if (blocked) {
encoderBlockedStreams.add(streamId);
}
else {
encoderBlockedStreams.remove(streamId);
}
}
public synchronized void incrementEncoderNotOpenedError(String streamId) {
numberOfEncoderNotOpenedErrors ++;
Broadcast broadcast = getDataStore().get(streamId);
if (broadcast != null) {
final String listenerHookURL = getListenerHookURL(broadcast);
if (listenerHookURL != null && listenerHookURL.length() > 0) {
final String name = broadcast.getName();
final String category = broadcast.getCategory();
final String metaData = broadcast.getMetaData();
String mainTrackId = broadcast.getMainTrackStreamId();
logger.info("Setting timer to call encoder not opened error for stream:{}", streamId);
notifyHook(listenerHookURL, streamId, mainTrackId, HOOK_ACTION_ENCODER_NOT_OPENED_ERROR, name, category, null, null, metaData, null);
}
}
}
public int getNumberOfEncoderNotOpenedErrors() {
return numberOfEncoderNotOpenedErrors;
}
public int getNumberOfPublishTimeoutError() {
return publishTimeoutStreams;
}
public synchronized void publishTimeoutError(String streamId, String subscriberId) {
publishTimeoutStreams++;
publishTimeoutStreamsList.add(streamId);
Broadcast broadcast = getDataStore().get(streamId);
if (broadcast != null)
{
final String listenerHookURL = getListenerHookURL(broadcast);
if (listenerHookURL != null && listenerHookURL.length() > 0) {
final String name = broadcast.getName();
final String category = broadcast.getCategory();
String mainTrackId = broadcast.getMainTrackStreamId();
logger.info("Setting timer to call hook that means live stream is not started to the publish timeout for stream:{}", streamId);
JSONObject jsonResponse = new JSONObject();
jsonResponse.put(WebSocketConstants.SUBSCRIBER_ID, subscriberId);
notifyHook(listenerHookURL, streamId, mainTrackId, HOOK_ACTION_PUBLISH_TIMEOUT_ERROR, name, category, null, null, jsonResponse.toJSONString(), null);
}
}
}
public WebRTCAudioReceiveStats getWebRTCAudioReceiveStats() {
return webRTCAudioReceiveStats;
}
public WebRTCVideoReceiveStats getWebRTCVideoReceiveStats() {
return webRTCVideoReceiveStats;
}
public WebRTCAudioSendStats getWebRTCAudioSendStats() {
return webRTCAudioSendStats;
}
public WebRTCVideoSendStats getWebRTCVideoSendStats() {
return webRTCVideoSendStats;
}
public Vertx getVertx() {
if (vertx == null) {
vertx = (Vertx) getScope().getContext().getBean(VERTX_BEAN_NAME);
}
return vertx;
}
/*
* This method can be called by multiple threads especially in cluster mode
* and this cause some issues for settings synchronization. So that it's synchronized
*
* @param newSettings
* @param notifyCluster
* @param checkUpdateTime, if it is false it checks the update time of the currents settings and incoming settings.
* If the incoming setting is older than current settings, it returns false.
* If it is false, it just writes the settings without checking time
* @return
*/
public synchronized boolean updateSettings(AppSettings newSettings, boolean notifyCluster, boolean checkUpdateTime) {
boolean result = false;
if (checkUpdateTime && !isIncomingSettingsDifferent(newSettings)) {
//if current app settings update time is bigger than the newSettings, don't update the bean
//it may happen in cluster mode, app settings may be updated locally then a new update just may come instantly from cluster settings.
logger.debug("Not saving the settings because current appsettings update time({}) incoming settings update time({}) are same", appSettings.getUpdateTime(), newSettings.getUpdateTime() );
return result;
}
//if there is any wrong encoder settings, return false
List encoderSettingsList = newSettings.getEncoderSettings();
if (!isEncoderSettingsValid(encoderSettingsList)) {
return result;
}
//if there is any wrong publish/play token settings, return false.
//A single token security setting can be activated for both publishing and playing, with a maximum limit of two settings—one for each.
if(!isTokenSecuritySettingsValid(newSettings)){
logger.info("Could not save app settings. Only one type of token control should be enabled for publish or play.");
return result;
}
//synch again because of string to list mapping- TODO: There is a better way for string to list mapping
//in properties files
newSettings.setEncoderSettings(encoderSettingsList);
if (newSettings.getHlsListSize() == null || Integer.valueOf(newSettings.getHlsListSize()) < 5) {
newSettings.setHlsListSize("5");
}
if (newSettings.getHlsTime() == null || Integer.valueOf(newSettings.getHlsTime()) < 1) {
newSettings.setHlsTime("1");
}
/**
* ATTENTION: When a new settings added both
* {@link #updateAppSettingsFile} && {@link #updateAppSettingsBean} should be updated
*/
updateAppSettingsBean(appSettings, newSettings, notifyCluster);
AcceptOnlyStreamsInDataStore securityHandler = (AcceptOnlyStreamsInDataStore) getScope().getContext().getBean(AcceptOnlyStreamsInDataStore.BEAN_NAME);
securityHandler.setEnabled(newSettings.isAcceptOnlyStreamsInDataStore());
if (notifyCluster && clusterNotifier != null) {
//we should set to be deleted because app deletion fully depends on the cluster synch TODO remove the following line because toBeDeleted is deprecated
appSettings.setToBeDeleted(newSettings.isToBeDeleted());
appSettings.setAppStatus(newSettings.getAppStatus());
boolean saveSettings = clusterNotifier.getClusterStore().saveSettings(appSettings);
logger.info("Saving settings to cluster db -> {} for app: {} and updateTime:{}", saveSettings, getScope().getName(), appSettings.getUpdateTime());
}
if (updateAppSettingsFile(getScope().getName(), newSettings))
{
result = true;
}
else {
logger.warn("Settings cannot be saved for {}", getScope().getName());
}
return result;
}
private boolean isEncoderSettingsValid(List encoderSettingsList) {
if (encoderSettingsList != null) {
for (Iterator iterator = encoderSettingsList.iterator(); iterator.hasNext();) {
EncoderSettings encoderSettings = iterator.next();
if (encoderSettings.getHeight() <= 0)
{
logger.error("Unexpected encoder parameter. None of the parameters(height:{}, video bitrate:{}, audio bitrate:{}) can be zero or less", encoderSettings.getHeight(), encoderSettings.getVideoBitrate(), encoderSettings.getAudioBitrate());
return false;
}
}
}
return true;
}
private boolean isTokenSecuritySettingsValid(AppSettings newSettings) {
int publishTokenSecurityEnabledCount = 0;
int playTokenSecurityEnabledCount = 0;
if (newSettings.isPublishTokenControlEnabled()) {
publishTokenSecurityEnabledCount++;
}
if (newSettings.isPublishJwtControlEnabled()) {
publishTokenSecurityEnabledCount++;
}
if (newSettings.isEnableTimeTokenForPublish()) {
publishTokenSecurityEnabledCount++;
}
if (newSettings.isEnableTimeTokenForPlay()) {
playTokenSecurityEnabledCount++;
}
if (newSettings.isPlayTokenControlEnabled()) {
playTokenSecurityEnabledCount++;
}
if (newSettings.isPlayJwtControlEnabled()) {
playTokenSecurityEnabledCount++;
}
// Only one type of token control should be enabled for publish and play
return publishTokenSecurityEnabledCount <= 1 && playTokenSecurityEnabledCount <= 1;
}
/**
*
* @param newSettings
* @param checkUpdateTime
* @return true if time are not equal, it means new settings is different than the current settings
*/
public boolean isIncomingSettingsDifferent(AppSettings newSettings)
{
return appSettings.getUpdateTime() != newSettings.getUpdateTime();
}
public void setClusterNotifier(IClusterNotifier clusterNotifier) {
this.clusterNotifier = clusterNotifier;
}
public static boolean updateAppSettingsFile(String appName, AppSettings newAppsettings)
{
/*
* Remember remember the 23th of November
*
* String.valueof(null) returns "null" string.
*
* If we know the case above, we will write better codes.
*
*/
PreferenceStore store = new PreferenceStore(WEBAPPS_PATH + appName + "/WEB-INF/red5-web.properties");
Field[] declaredFields = newAppsettings.getClass().getDeclaredFields();
for (Field field : declaredFields)
{
if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers()))
{
if (field.trySetAccessible())
{
try {
Object value = field.get(newAppsettings);
if (value instanceof List) {
store.put(field.getName(), AppSettings.encodersList2Str(newAppsettings.getEncoderSettings()));
}
else if (value instanceof Map) {
store.put(field.getName(), new JSONObject((Map) value).toJSONString());
}
else {
store.put(field.getName(), value != null ? String.valueOf(value) : "");
}
}
catch (IllegalArgumentException | IllegalAccessException e) {
logger.error(ExceptionUtils.getStackTrace(e));
}
field.setAccessible(false);
}
}
}
return store.save();
}
public void updateAppSettingsBean(AppSettings appSettings, AppSettings newSettings, boolean updateTime)
{
Field[] declaredFields = appSettings.getClass().getDeclaredFields();
for (Field field : declaredFields)
{
setAppSettingsFieldValue(appSettings, newSettings, field);
}
if (updateTime) {
//updateTime is true when the app settings is updated from the REST API or it's first updated when the app starts first in the cluster
appSettings.setUpdateTime(System.currentTimeMillis());
}
String oldVodFolder = appSettings.getVodFolder();
synchUserVoDFolder(oldVodFolder, newSettings.getVodFolder());
setStorageclientSettings(newSettings);
logger.warn("app settings bean updated for {}", getScope().getName());
}
public void setStorageclientSettings(AppSettings settings) {
storageClient.setEndpoint(settings.getS3Endpoint());
storageClient.setStorageName(settings.getS3BucketName());
storageClient.setAccessKey(settings.getS3AccessKey());
storageClient.setSecretKey(settings.getS3SecretKey());
storageClient.setRegion(settings.getS3RegionName());
storageClient.setEnabled(settings.isS3RecordingEnabled());
storageClient.setPermission(settings.getS3Permission());
storageClient.setStorageClass(settings.getS3StorageClass());
storageClient.setCacheControl(settings.getS3CacheControl());
storageClient.reset();
}
public static boolean setAppSettingsFieldValue(AppSettings appSettings, AppSettings newSettings, Field field) {
boolean result = false;
try {
if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
if (field.trySetAccessible())
{
field.set(appSettings, field.get(newSettings));
field.setAccessible(false);
result = true;
}
else
{
logger.warn("Cannot set the value this field: {}", field.getName());
}
}
}
catch (IllegalArgumentException | IllegalAccessException e)
{
logger.error(ExceptionUtils.getStackTrace(e));
}
return result;
}
public void setServerSettings(ServerSettings serverSettings) {
this.serverSettings = serverSettings;
}
/*
* This method is overridden in enterprise edition since RTMP to WebRTC streaming is an enterprise feature.
*/
public RTMPToWebRTCStats getRTMPToWebRTCStats(String streamId) {
return new RTMPToWebRTCStats(streamId);
}
public boolean isDataChannelEnabled() {
return false;
}
public boolean isDataChannelMessagingSupported() {
return false;
}
public boolean sendDataChannelMessage(String streamId, String message) {
return false;
}
public boolean doesWebRTCStreamExist(String streamId) {
return false;
}
public boolean addPacketListener(String streamId, IPacketListener listener) {
boolean isAdded = false;
MuxAdaptor muxAdaptorsLocal = getMuxAdaptor(streamId);
if (muxAdaptorsLocal != null) {
muxAdaptorsLocal.addPacketListener(listener);
logger.info("Packet listener({}) is added to streamId:{}", listener.getClass().getSimpleName(), streamId);
isAdded = true;
}
if(!isAdded) {
logger.info("Stream:{} is not in this server. It's creating cluster stream fetcher to get the stream", streamId);
if(clusterStreamFetcher == null) {
clusterStreamFetcher = createClusterStreamFetcher();
}
isAdded = clusterStreamFetcher.register(streamId, listener);
}
return isAdded;
}
public void endpointFailedUpdate(String streamId, String url) {
Broadcast broadcast = getDataStore().get(streamId);
if (broadcast != null)
{
final String listenerHookURL = getListenerHookURL(broadcast);
if (listenerHookURL != null && listenerHookURL.length() > 0)
{
final String name = broadcast.getName();
final String category = broadcast.getCategory();
String mainTrackId = broadcast.getMainTrackStreamId();
logger.info("Setting timer to call rtmp endpoint failed hook for stream:{}", streamId);
JSONObject jsonObject = new JSONObject();
jsonObject.put("rtmp-url", url);
notifyHook(listenerHookURL, streamId, mainTrackId, HOOK_ACTION_ENDPOINT_FAILED, name, category, null, null, jsonObject.toJSONString(), null);
}
}
}
public boolean removePacketListener(String streamId, IPacketListener listener) {
boolean isRemoved = false;
MuxAdaptor muxAdaptorsLocal = getMuxAdaptor(streamId);
if (muxAdaptorsLocal != null) {
isRemoved = muxAdaptorsLocal.removePacketListener(listener);
}
if (!isRemoved)
{
if (clusterStreamFetcher != null)
{
isRemoved = clusterStreamFetcher.remove(streamId, listener);
}
else {
logger.warn("Cluster stream fetcher is null so that packet listener cannot be removed for streamId:{}", streamId);
}
}
if (isRemoved) {
logger.info("Packet listener is removed succesfully from adaptor for streamId:{}", streamId);
}
else {
logger.warn("Packet listener cannot be removed from adaptor for streamId:{}", streamId);
}
return isRemoved;
}
public void addFrameListener(String streamId, IFrameListener listener) {
//for enterprise
}
public IFrameListener createCustomBroadcast(String streamId) {
throw new IllegalStateException("This method is not implemented in Community Edition");
}
public void stopCustomBroadcast(String streamId) {
}
public void removeFrameListener(String streamId, IFrameListener listener) {
}
@Override
public boolean isServerShuttingDown() {
return serverShuttingDown;
}
public void setStorageClient(StorageClient storageClient) {
this.storageClient = storageClient;
}
public StorageClient getStorageClient() {
return storageClient;
}
public void addStreamListener(IStreamListener listener) {
streamListeners.add(listener);
}
public void removeStreamListener(IStreamListener listener) {
streamListeners.remove(listener);
}
public boolean stopPlaying(String viewerId) {
return false;
}
public CompletableFuture startHttpSignaling(PublishParameters publishParameters, String sdp, String sessionId){
//for enterprise
return null;
}
public Result stopWhipBroadcast(String streamId, String sessionId){
return new Result(false);
}
public boolean stopPlayingBySubscriberId(String subscriberId){
return false;
}
public boolean stopPublishingBySubscriberId(String subscriberId){
return false;
}
public void stopPublish(String streamId) {
vertx.executeBlocking(() -> {
closeBroadcast(streamId);
return null;
});
}
public void joinedTheRoom(String roomId, String streamId) {
//No need to implement here.
}
public void leftTheRoom(String mainTrackId, String subtrackId) {
//No need to implement here.
}
public IClusterStreamFetcher createClusterStreamFetcher() {
return null;
}
public Map> getWebRTCClientsMap() {
return Collections.emptyMap();
}
public ISubtrackPoller getSubtrackPoller() {
return subtrackPoller;
}
public void setSubtrackPoller(ISubtrackPoller subtrackPoller) {
this.subtrackPoller = subtrackPoller;
}
public Map getPlayListSchedulerTimer() {
return playListSchedulerTimer;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy