
io.antmedia.streamsource.StreamFetcherManager Maven / Gradle / Ivy
package io.antmedia.streamsource;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.red5.server.api.scope.IScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.antmedia.AntMediaApplicationAdapter;
import io.antmedia.AppSettings;
import io.antmedia.datastore.db.DataStore;
import io.antmedia.datastore.db.types.Broadcast;
import io.antmedia.datastore.db.types.BroadcastUpdate;
import io.antmedia.datastore.db.types.Broadcast.PlayListItem;
import io.antmedia.licence.ILicenceService;
import io.antmedia.muxer.IAntMediaStreamHandler;
import io.antmedia.muxer.MuxAdaptor;
import io.antmedia.rest.model.Result;
import io.antmedia.settings.ServerSettings;
import io.antmedia.shutdown.AMSShutdownManager;
import io.antmedia.streamsource.StreamFetcher.IStreamFetcherListener;
import io.vertx.core.Vertx;
/**
* Organizes and checks stream fetcher and restarts them if it is required
* @author davut
*
*/
public class StreamFetcherManager {
protected static Logger logger = LoggerFactory.getLogger(StreamFetcherManager.class);
private int streamCheckerCount = 0;
private Map streamFetcherList = new ConcurrentHashMap<>();
/**
* Time period in milli seconds for checking stream fetchers status, restart issues etc.
* It's the same value with MuxAdaptor.STAT_UPDATE_PERIOD_MS because it updates the database record and let us understand if stream is alive
* with {@link AntMediaApplicationAdapter#isStreaming(Broadcast)}
*
*/
private int streamCheckerIntervalMs = MuxAdaptor.STAT_UPDATE_PERIOD_MS;
private DataStore datastore;
private IScope scope;
private long streamFetcherScheduleJobName = -1L;
protected AtomicBoolean isJobRunning = new AtomicBoolean(false);
private boolean restartStreamAutomatically = true;
private Vertx vertx;
private int lastRestartCount;
private AppSettings appSettings;
private ILicenceService licenseService;
boolean serverShuttingDown = false;
private ServerSettings serverSettings;
public StreamFetcherManager(Vertx vertx, DataStore datastore,IScope scope) {
this.vertx = vertx;
this.datastore = datastore;
this.scope=scope;
this.appSettings = (AppSettings) scope.getContext().getBean(AppSettings.BEAN_NAME);
this.serverSettings = (ServerSettings) scope.getContext().getBean(ServerSettings.BEAN_NAME);
this.licenseService = (ILicenceService)scope.getContext().getBean(ILicenceService.BeanName.LICENCE_SERVICE.toString());
AMSShutdownManager.getInstance().subscribe(()-> shuttingDown());
}
public void shuttingDown() {
serverShuttingDown = true;
}
public StreamFetcher make(Broadcast stream, IScope scope, Vertx vertx) {
return new StreamFetcher(stream.getStreamUrl(), stream.getStreamId(), stream.getType(), scope, vertx, stream.getSeekTimeInMs());
}
public int getStreamCheckerInterval() {
return streamCheckerIntervalMs;
}
/**
* Set stream checker interval, this value is used in periodically checking
* the status of the stream fetchers
*
* @param streamCheckerInterval, time period of the stream fetcher check interval in milliseconds
*/
public void testSetStreamCheckerInterval(int streamCheckerInterval) {
this.streamCheckerIntervalMs = streamCheckerInterval;
}
public boolean isStreamRunning(Broadcast broadcast) {
boolean isStreamLive = false;
if (streamFetcherList.containsKey(broadcast.getStreamId())) {
logger.info("Stream is still on FetcherManagerList so it's active for streamId:{}", broadcast.getStreamId());
isStreamLive = true;
}
if (!isStreamLive) {
//this stream may be fetching in somewhere in the cluster
isStreamLive = AntMediaApplicationAdapter.isStreaming(broadcast) &&
AntMediaApplicationAdapter.isInstanceAlive(broadcast.getOriginAdress(), serverSettings.getHostAddress(), serverSettings.getDefaultHttpPort(), scope.getName());
}
return isStreamLive;
}
public Result startStreamScheduler(StreamFetcher streamScheduler) {
Result result = new Result(false);
result.setDataId(streamScheduler.getStreamId());
if (!licenseService.isLicenceSuspended()) {
logger.info("Starting stream fetcher for streamId:{}", streamScheduler.getStreamId());
streamScheduler.startStream();
if (streamFetcherList.containsKey(streamScheduler.getStreamId())) {
//this log has been put while we refactor streamFetcherList
logger.warn("There is already a stream schedule exists for streamId:{} ", streamScheduler.getStreamId());
}
streamFetcherList.put(streamScheduler.getStreamId(), streamScheduler);
if (streamFetcherScheduleJobName == -1) {
scheduleStreamFetcherJob();
}
result.setSuccess(true);
}
else {
logger.error("License is suspend and new stream scheduler is not started {}", streamScheduler.getStreamUrl());
result.setMessage("License is suspended");
}
return result;
}
public Result startStreaming(@Nonnull Broadcast broadcast) {
//check if broadcast is already being fetching
boolean alreadyFetching = isStreamRunning(broadcast);
//FYI: Even ff the stream is trying to prepare in any node in the cluster, alreadyFetching returns false to not have any duplication
StreamFetcher streamScheduler = null;
Result result = new Result(false);
if (!alreadyFetching) {
try {
streamScheduler = make(broadcast, scope, vertx);
streamScheduler.setRestartStream(restartStreamAutomatically);
streamScheduler.setDataStore(getDatastore());
result = startStreamScheduler(streamScheduler);
}
catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
result.setMessage("Problem occured while fetching the stream");
}
}
else {
logger.info("Stream is already active for streamId:{}", broadcast.getStreamId());
result.setMessage("Stream is already active. It's already streaming or trying to connect");
}
return result;
}
public Result stopStreaming(String streamId)
{
logger.warn("inside of stopStreaming for {}", streamId);
Result result = new Result(false);
if (StringUtils.isNotBlank(streamId))
{
StreamFetcher scheduler = streamFetcherList.remove(streamId);
if (scheduler != null) {
scheduler.stopStream();
result.setSuccess(true);
}
}
result.setMessage(result.isSuccess() ? "Stream stopped" : "No matching stream source in this server:"+streamId);
result.setDataId(streamId);
return result;
}
public void stopCheckerJob() {
if (streamFetcherScheduleJobName != -1) {
vertx.cancelTimer(streamFetcherScheduleJobName);
streamFetcherScheduleJobName = -1;
}
}
public static Result checkStreamUrlWithHTTP(String url){
Result result = new Result(false);
URL checkUrl;
HttpURLConnection huc;
int responseCode;
try {
checkUrl = new URL(url);
huc = (HttpURLConnection) checkUrl.openConnection();
responseCode = huc.getResponseCode();
if(responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MOVED_PERM ) {
result.setSuccess(true);
return result;
}
else {
result.setSuccess(false);
result.setMessage("URL "+url+ "responded:"+responseCode);
return result;
}
} catch (IOException e) {
result.setSuccess(false);
}
return result;
}
public void playNextItemInList(String streamId, IStreamFetcherListener listener) {
// Get current playlist in database, it may be updated
Broadcast playlist = datastore.get(streamId);
if (playlist != null) {
playItemInList(playlist, listener, -1);
}
}
/**
*
* @param playlist
* @param listener
* @param index if it's -1, it plays the next item, if it's zero or bigger, it skips that item to play
*/
public Result playItemInList(Broadcast playlist, IStreamFetcherListener listener, int index)
{
// It's necessary for skip new Stream Fetcher
stopStreaming(playlist.getStreamId());
Result result = new Result(false);
if (serverShuttingDown) {
logger.info("Playlist will not try to play the next item because server is shutting down");
result.setMessage("Playlist will not try to play the next item because server is shutting down");
return result;
}
//Check playlist is not stopped and there is an item to play
if(!IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED.equals(playlist.getPlayListStatus())
&& skipNextPlaylistQueue(playlist, index) != null)
{
// Get Current Playlist Stream Index
int currentStreamIndex = playlist.getCurrentPlayIndex();
// Check Stream URL is valid.
// If stream URL is not valid, it's trying next broadcast and trying.
if(checkStreamUrlWithHTTP(playlist.getPlayListItemList().get(currentStreamIndex).getStreamUrl()).isSuccess())
{
//update broadcast informations
PlayListItem fetchedBroadcast = playlist.getPlayListItemList().get(currentStreamIndex);
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setPlayListStatus(playlist.getPlayListStatus());
broadcastUpdate.setCurrentPlayIndex(playlist.getCurrentPlayIndex());
datastore.updateBroadcastFields(playlist.getStreamId(), broadcastUpdate);
StreamFetcher newStreamScheduler = new StreamFetcher(fetchedBroadcast.getStreamUrl(), playlist.getStreamId(), fetchedBroadcast.getType(), scope,vertx, fetchedBroadcast.getSeekTimeInMs());
newStreamScheduler.setStreamFetcherListener(listener);
newStreamScheduler.setRestartStream(false);
result = startStreamScheduler(newStreamScheduler);
}
else
{
logger.info("Current Playlist Stream URL -> {} is invalid", playlist.getPlayListItemList().get(currentStreamIndex).getStreamUrl());
playlist = skipNextPlaylistQueue(playlist, -1);
result = startPlaylist(playlist);
}
}
else {
result.setMessage("Playlist is either stopped or there is no item to play");
}
return result;
}
public Result startPlaylist(Broadcast playlist){
Result result = new Result(false);
List playListItemList = playlist.getPlayListItemList();
if (isStreamRunning(playlist))
{
logger.warn("Playlist is already running for stream:{}", playlist.getStreamId());
String msg = "Playlist is already running for stream:"+playlist.getStreamId();
logger.warn(msg);
result.setMessage(msg);
}
else if (playListItemList != null && !playListItemList.isEmpty())
{
// Get current stream in Playlist
if (playlist.getCurrentPlayIndex() >= playlist.getPlayListItemList().size() || playlist.getCurrentPlayIndex() < 0) {
logger.warn("Resetting current play index to 0 because it's not in correct range for id: {}", playlist.getStreamId());
playlist.setCurrentPlayIndex(0);
}
PlayListItem playlistBroadcastItem = playlist.getPlayListItemList().get(playlist.getCurrentPlayIndex());
if(checkStreamUrlWithHTTP(playlistBroadcastItem.getStreamUrl()).isSuccess())
{
logger.info("Starting playlist item:{} for streamId:{}", playlistBroadcastItem.getStreamUrl(), playlist.getStreamId());
// Check Stream URL is valid.
// If stream URL is not valid, it's trying next broadcast and trying.
// Create Stream Fetcher with Playlist Broadcast Item
StreamFetcher streamScheduler = new StreamFetcher(playlistBroadcastItem.getStreamUrl(), playlist.getStreamId(), playlistBroadcastItem.getType(), scope, vertx, playlistBroadcastItem.getSeekTimeInMs());
// Update Playlist current playing status
playlist.setPlayListStatus(IAntMediaStreamHandler.BROADCAST_STATUS_BROADCASTING);
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setPlayListStatus(playlist.getPlayListStatus());
broadcastUpdate.setCurrentPlayIndex(playlist.getCurrentPlayIndex());
// Update Datastore current play broadcast
datastore.updateBroadcastFields(playlist.getStreamId(), broadcastUpdate);
String streamId = playlist.getStreamId();
streamScheduler.setStreamFetcherListener(listener -> {
playNextItemInList(streamId, listener);
});
streamScheduler.setRestartStream(false);
startStreamScheduler(streamScheduler);
result.setSuccess(true);
}
else
{
logger.warn("Current Playlist Stream URL -> {} is invalid", playlistBroadcastItem.getStreamUrl());
// This method skip next playlist item
playlist = skipNextPlaylistQueue(playlist, -1);
if(checkStreamUrlWithHTTP(playlist.getPlayListItemList().get(playlist.getCurrentPlayIndex()).getStreamUrl()).isSuccess()) {
result = startPlaylist(playlist);
}
else {
playlist.setStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
// Update Datastore current play broadcast
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setStatus(playlist.getStatus());
broadcastUpdate.setCurrentPlayIndex(playlist.getCurrentPlayIndex());
datastore.updateBroadcastFields(playlist.getStreamId(), broadcastUpdate);
result.setSuccess(false);
}
}
}
else {
String msg = "There is no playlist for stream id:" + playlist.getStreamId();
logger.warn(msg);
result.setMessage(msg);
}
return result;
}
/**
* Skips the next item or set to first item in the list. If the looping is disabled, it will not set to first item and return nul
* @param playlist
* @param index: if it's -1, plays the next item, otherwise it plays the item that is in the index
* @return Broadcast object for the next item. If it's not looping, it will return null
*/
public Broadcast skipNextPlaylistQueue(Broadcast playlist, int index) {
// Get Current Playlist Stream Index
int currentStreamIndex = index;
if (index < 0) {
currentStreamIndex = playlist.getCurrentPlayIndex()+1;
}
if(playlist.getPlayListItemList().size() <= currentStreamIndex)
{
//update playlist first broadcast
playlist.setCurrentPlayIndex(0);
if (!playlist.isPlaylistLoopEnabled())
{
logger.info("Play list looping is not enabled. It will be stopped for stream: {}", playlist.getStreamId());
//streaming is already stopped so that just update the database
playlist.setPlayListStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setPlayListStatus(playlist.getPlayListStatus());
broadcastUpdate.setCurrentPlayIndex(playlist.getCurrentPlayIndex());
datastore.updateBroadcastFields(playlist.getStreamId(), broadcastUpdate);
//return null if it's not looping
return null;
}
else {
logger.info("Playlist has finished and playlist loop is enabled so setting index to 0 for playlist:{}", playlist.getStreamId());
}
}
else {
// update playlist currentPlayIndex value.
playlist.setCurrentPlayIndex(currentStreamIndex);
}
logger.info("Next index to play in play list is {} for stream: {}", playlist.getCurrentPlayIndex(), playlist.getStreamId());
return playlist;
}
private void scheduleStreamFetcherJob() {
if (streamFetcherScheduleJobName != -1) {
vertx.cancelTimer(streamFetcherScheduleJobName);
}
streamFetcherScheduleJobName = vertx.setPeriodic(streamCheckerIntervalMs, l-> {
if (!streamFetcherList.isEmpty()) {
streamCheckerCount++;
logger.debug("StreamFetcher Check Count:{}" , streamCheckerCount);
int countToRestart = 0;
int restartStreamFetcherPeriodSeconds = appSettings.getRestartStreamFetcherPeriod();
if (restartStreamFetcherPeriodSeconds > 0)
{
int streamCheckIntervalSec = streamCheckerIntervalMs / 1000;
countToRestart = (streamCheckerCount * streamCheckIntervalSec) / restartStreamFetcherPeriodSeconds;
}
boolean restart = countToRestart > lastRestartCount;
if (restart) {
lastRestartCount = countToRestart;
logger.info("This is {} times that restarting streams", lastRestartCount);
}
controlStreamFetchers(restart);
}
});
logger.info("StreamFetcherSchedule job name {}", streamFetcherScheduleJobName);
}
public boolean isToBeStoppedAutomatically(Broadcast broadcast)
{
// broadcast autoStartEnabled and there is nobody watching and it's started more than streamCheckerIntervalMs ago
logger.info("broadcast is autoStartStopEnabled:{} isAnyonewatching:{} startTime:{} streamCheckerIntervalMs:{}",
broadcast.isAutoStartStopEnabled(), broadcast.isAnyoneWatching(), broadcast.getStartTime(), streamCheckerIntervalMs);
return broadcast.isAutoStartStopEnabled() && !broadcast.isAnyoneWatching() &&
broadcast.getStartTime() != 0 && (System.currentTimeMillis() > (broadcast.getStartTime() + streamCheckerIntervalMs));
}
public void controlStreamFetchers(boolean restart) {
for (StreamFetcher streamScheduler : streamFetcherList.values()) {
//get the updated broadcast object
Broadcast broadcast = datastore.get(streamScheduler.getStreamId());
if (broadcast != null && AntMediaApplicationAdapter.PLAY_LIST.equals(broadcast.getType())) {
//if it's playlist, continue
continue;
}
boolean autoStop = false;
if (restart || broadcast == null ||
(autoStop = isToBeStoppedAutomatically(broadcast)))
{
//logic of If condition is
// stop it if it's restart = true
// or
// brodcast == null because it means stream is deleted
// or
// autoStop
logger.info("Calling stop stream {} due to restart -> {}, broadcast is null -> {}, auto stop because no viewer -> {}",
streamScheduler.getStreamId(), restart, broadcast == null, autoStop);
stopStreaming(streamScheduler.getStreamId());
}
else {
logger.info("Stream:{} is alive -> {}, is it blocked -> {}", streamScheduler.getStreamId(), streamScheduler.isStreamAlive(), streamScheduler.isStreamBlocked());
//stream blocked means there is a connection to stream source and it's waiting to read a new packet
//Most of the time the problem is related to the stream source side.
}
//start streaming if broadcast object is in db(it means not deleted)
if (restart && broadcast != null)
{
//it may be still running because stop operation is async
//So start streaming after it's finished
if (isStreamRunning(broadcast))
{
logger.info("Setting stream fetcher listener to restart when it's finished for streamId:{}", broadcast.getStreamId());
streamScheduler.setStreamFetcherListener((l) -> {
//Get the updated version because we don't know when it's called and we need up to date info
Broadcast freshBroadcast = datastore.get(streamScheduler.getStreamId());
if (freshBroadcast != null) {
startStreaming(freshBroadcast);
}
});
}
else {
startStreaming(broadcast);
}
}
}
}
public DataStore getDatastore() {
return datastore;
}
public void setDatastore(DataStore datastore) {
this.datastore = datastore;
}
public Map getStreamFetcherList() {
return streamFetcherList;
}
public StreamFetcher getStreamFetcher(String streamId)
{
return streamFetcherList.get(streamId);
}
public void setStreamFetcherList(Map streamFetcherList) {
this.streamFetcherList = streamFetcherList;
}
public boolean isRestartStreamAutomatically() {
return restartStreamAutomatically;
}
public void setRestartStreamAutomatically(boolean restartStreamAutomatically) {
this.restartStreamAutomatically = restartStreamAutomatically;
}
public int getStreamCheckerCount() {
return streamCheckerCount;
}
public void setStreamCheckerCount(int streamCheckerCount) {
this.streamCheckerCount = streamCheckerCount;
}
public Result stopPlayList(String streamId)
{
logger.info("Stopping playlist for stream: {}", streamId);
Result result = stopStreaming(streamId);
if (result.isSuccess())
{
result = new Result(false);
Broadcast broadcast = datastore.get(streamId);
if (broadcast != null && AntMediaApplicationAdapter.PLAY_LIST.equals(broadcast.getType()))
{
broadcast.setPlayListStatus(IAntMediaStreamHandler.BROADCAST_STATUS_FINISHED);
BroadcastUpdate broadcastUpdate = new BroadcastUpdate();
broadcastUpdate.setPlayListStatus(broadcast.getPlayListStatus());
result.setSuccess(datastore.updateBroadcastFields(streamId, broadcastUpdate));
}
else {
String msg = "Broadcast's type is not play list for stream:" + streamId;
result.setMessage(msg);
result.setDataId(streamId);
logger.error(msg);
}
}
else {
logger.warn("Stop streamming returned false for stream:{} message:{}", streamId, result.getMessage());
}
return result;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy