org.red5.server.stream.PlaylistSubscriberStream 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.
/*
* RED5 Open Source Media Server - https://github.com/Red5/
*
* Copyright 2006-2016 by respective authors (see below). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.red5.server.stream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IConnection;
import org.red5.server.api.IContext;
import org.red5.server.api.Red5;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.statistics.IPlaylistSubscriberStreamStatistics;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IPlaylistController;
import org.red5.server.api.stream.IPlaylistSubscriberStream;
import org.red5.server.api.stream.IStreamAwareScopeHandler;
import org.red5.server.api.stream.OperationNotSupportedException;
import org.red5.server.api.stream.StreamState;
import org.slf4j.Logger;
/**
* Stream of playlist subscriber
*/
public class PlaylistSubscriberStream extends AbstractClientStream implements IPlaylistSubscriberStream, IPlaylistSubscriberStreamStatistics {
private static final Logger log = Red5LoggerFactory.getLogger(PlaylistSubscriberStream.class);
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();
/**
* Playlist controller
*/
private IPlaylistController controller;
/**
* Default playlist controller
*/
private IPlaylistController defaultController;
/**
* Playlist items
*/
private final LinkedList items;
/**
* Current item index
*/
private int currentItemIndex = 0;
/**
* Plays items back
*/
protected PlayEngine engine;
/**
* Rewind mode state
*/
protected boolean rewind;
/**
* Random mode state
*/
protected boolean random;
/**
* Repeat mode state
*/
protected boolean repeat;
/**
* Service used to provide notifications, keep client buffer filled, clean up, etc...
*/
protected ISchedulingService schedulingService;
/**
* Scheduled job names
*/
protected CopyOnWriteArraySet jobs = new CopyOnWriteArraySet();
/**
* Interval in ms to check for buffer underruns in VOD streams.
*/
protected int bufferCheckInterval = 0;
/**
* Number of pending messages at which a
*
*
* NetStream.Play.InsufficientBW
*
*
* message is generated for VOD streams.
*/
protected int underrunTrigger = 10;
/**
* Timestamp this stream was created.
*/
protected long creationTime = System.currentTimeMillis();
/**
* Number of bytes sent.
*/
protected long bytesSent = 0;
/** Constructs a new PlaylistSubscriberStream. */
public PlaylistSubscriberStream() {
defaultController = new SimplePlaylistController();
items = new LinkedList();
}
/**
* Creates a play engine based on current services (scheduling service, consumer service, and provider service). This method is useful
* during unit testing.
*/
PlayEngine createEngine(ISchedulingService schedulingService, IConsumerService consumerService, IProviderService providerService) {
engine = new PlayEngine.Builder(this, schedulingService, consumerService, providerService).build();
return engine;
}
/**
* Set interval to check for buffer underruns. Set to 0 to disable.
*
* @param bufferCheckInterval
* interval in ms
*/
public void setBufferCheckInterval(int bufferCheckInterval) {
this.bufferCheckInterval = bufferCheckInterval;
}
/**
* Set maximum number of pending messages at which a
*
*
* NetStream.Play.InsufficientBW
*
*
* message will be generated for VOD streams
*
* @param underrunTrigger
* the maximum number of pending messages
*/
public void setUnderrunTrigger(int underrunTrigger) {
this.underrunTrigger = underrunTrigger;
}
/** {@inheritDoc} */
public void start() {
//ensure the play engine exists
if (engine == null) {
IScope scope = getScope();
if (scope != null) {
IContext ctx = scope.getContext();
if (ctx.hasBean(ISchedulingService.BEAN_NAME)) {
schedulingService = (ISchedulingService) ctx.getBean(ISchedulingService.BEAN_NAME);
} else {
//try the parent
schedulingService = (ISchedulingService) scope.getParent().getContext().getBean(ISchedulingService.BEAN_NAME);
}
IConsumerService consumerService = null;
if (ctx.hasBean(IConsumerService.KEY)) {
consumerService = (IConsumerService) ctx.getBean(IConsumerService.KEY);
} else {
//try the parent
consumerService = (IConsumerService) scope.getParent().getContext().getBean(IConsumerService.KEY);
}
IProviderService providerService = null;
if (ctx.hasBean(IProviderService.BEAN_NAME)) {
providerService = (IProviderService) ctx.getBean(IProviderService.BEAN_NAME);
} else {
//try the parent
providerService = (IProviderService) scope.getParent().getContext().getBean(IProviderService.BEAN_NAME);
}
engine = new PlayEngine.Builder(this, schedulingService, consumerService, providerService).build();
} else {
throw new IllegalStateException("Scope was null on start playing");
}
}
//set buffer check interval
engine.setBufferCheckInterval(bufferCheckInterval);
//set underrun trigger
engine.setUnderrunTrigger(underrunTrigger);
// Start playback engine
engine.start();
// Notify subscribers on start
onChange(StreamState.STARTED);
}
/** {@inheritDoc} */
public void play() throws IOException {
// Check how many is yet to play...
int count = items.size();
// Return if playlist is empty
if (count > 0) {
// Move to next if current item is set to -1
if (currentItemIndex == -1) {
moveToNext();
}
// If there's some more items on list then play current item
while (count-- > 0) {
IPlayItem item = null;
read.lock();
try {
// Get playlist item
item = items.get(currentItemIndex);
engine.play(item);
break;
} catch (StreamNotFoundException e) {
// go for next item
moveToNext();
if (currentItemIndex == -1) {
// we reaches the end.
break;
}
item = items.get(currentItemIndex);
} catch (IllegalStateException e) {
// an stream is already playing
break;
} finally {
read.unlock();
}
}
}
}
/** {@inheritDoc} */
public void pause(int position) {
try {
engine.pause(position);
} catch (IllegalStateException e) {
log.debug("pause caught an IllegalStateException");
}
}
/** {@inheritDoc} */
public void resume(int position) {
try {
engine.resume(position);
} catch (IllegalStateException e) {
log.debug("resume caught an IllegalStateException");
}
}
/** {@inheritDoc} */
public void stop() {
if (log.isDebugEnabled()) {
log.debug("stop");
}
try {
engine.stop();
} catch (IllegalStateException e) {
if (log.isTraceEnabled()) {
log.warn("stop caught an IllegalStateException", e);
} else if (log.isDebugEnabled()) {
log.debug("stop caught an IllegalStateException");
}
}
}
/** {@inheritDoc} */
public void seek(int position) throws OperationNotSupportedException {
try {
engine.seek(position);
} catch (IllegalStateException e) {
log.debug("seek caught an IllegalStateException");
}
}
/** {@inheritDoc} */
public void close() {
if (log.isDebugEnabled()) {
log.debug("close");
}
if (engine != null) {
// before or on close we may need to allow the queued messages a chance to clear
engine.close();
onChange(StreamState.CLOSED);
items.clear();
// clear jobs
if (schedulingService != null && !jobs.isEmpty()) {
for (String jobName : jobs) {
schedulingService.removeScheduledJob(jobName);
}
jobs.clear();
}
}
}
/** {@inheritDoc} */
public boolean isPaused() {
return state == StreamState.PAUSED;
}
/** {@inheritDoc} */
public void addItem(IPlayItem item) {
write.lock();
try {
items.add(item);
} finally {
write.unlock();
}
}
/** {@inheritDoc} */
public void addItem(IPlayItem item, int index) {
write.lock();
try {
items.add(index, item);
} finally {
write.unlock();
}
}
/** {@inheritDoc} */
public void removeItem(int index) {
if (index < 0 || index >= items.size()) {
return;
}
int originSize = items.size();
write.lock();
try {
items.remove(index);
} finally {
write.unlock();
}
if (currentItemIndex == index) {
// set the next item.
if (index == originSize - 1) {
currentItemIndex = index - 1;
}
}
}
/** {@inheritDoc} */
public void removeAllItems() {
// we try to stop the engine first
stop();
write.lock();
try {
items.clear();
} finally {
write.unlock();
}
}
/** {@inheritDoc} */
public void previousItem() {
stop();
moveToPrevious();
if (currentItemIndex == -1) {
return;
}
IPlayItem item = null;
int count = items.size();
while (count-- > 0) {
read.lock();
try {
item = items.get(currentItemIndex);
engine.play(item);
break;
} catch (IOException err) {
log.error("Error while starting to play item, moving to previous.", err);
// go for next item
moveToPrevious();
if (currentItemIndex == -1) {
// we reaches the end.
break;
}
} catch (StreamNotFoundException e) {
// go for next item
moveToPrevious();
if (currentItemIndex == -1) {
// we reaches the end.
break;
}
} catch (IllegalStateException e) {
// an stream is already playing
break;
} finally {
read.unlock();
}
}
}
/** {@inheritDoc} */
public boolean hasMoreItems() {
int nextItem = currentItemIndex + 1;
if (nextItem >= items.size() && !repeat) {
return false;
} else {
return true;
}
}
/** {@inheritDoc} */
public void nextItem() {
moveToNext();
if (currentItemIndex == -1) {
return;
}
IPlayItem item = null;
int count = items.size();
while (count-- > 0) {
read.lock();
try {
item = items.get(currentItemIndex);
engine.play(item, false);
break;
} catch (IOException err) {
log.error("Error while starting to play item, moving to next", err);
// go for next item
moveToNext();
if (currentItemIndex == -1) {
// we reaches the end.
break;
}
} catch (StreamNotFoundException e) {
// go for next item
moveToNext();
if (currentItemIndex == -1) {
// we reaches the end.
break;
}
} catch (IllegalStateException e) {
// an stream is already playing
break;
} finally {
read.unlock();
}
}
}
/** {@inheritDoc} */
public void setItem(int index) {
if (index < 0 || index >= items.size()) {
return;
}
stop();
currentItemIndex = index;
read.lock();
try {
IPlayItem item = items.get(currentItemIndex);
engine.play(item);
} catch (IOException e) {
log.error("setItem caught a IOException", e);
} catch (StreamNotFoundException e) {
// let the engine retain the STOPPED state
// and wait for control from outside
log.debug("setItem caught a StreamNotFoundException");
} catch (IllegalStateException e) {
log.error("Illegal state exception on playlist item setup", e);
} finally {
read.unlock();
}
}
/** {@inheritDoc} */
public boolean isRandom() {
return random;
}
/** {@inheritDoc} */
public void setRandom(boolean random) {
this.random = random;
}
/** {@inheritDoc} */
public boolean isRewind() {
return rewind;
}
/** {@inheritDoc} */
public void setRewind(boolean rewind) {
this.rewind = rewind;
}
/** {@inheritDoc} */
public boolean isRepeat() {
return repeat;
}
/** {@inheritDoc} */
public void setRepeat(boolean repeat) {
this.repeat = repeat;
}
/**
* Seek to current position to restart playback with audio and/or video.
*/
private void seekToCurrentPlayback() {
if (engine.isPullMode()) {
try {
// TODO: figure out if this is the correct position to seek to
final long delta = System.currentTimeMillis() - engine.getPlaybackStart();
engine.seek((int) delta);
} catch (OperationNotSupportedException err) {
// Ignore error, should not happen for pullMode engines
}
}
}
/** {@inheritDoc} */
public void receiveVideo(boolean receive) {
if (engine != null) {
boolean receiveVideo = engine.receiveVideo(receive);
if (!receiveVideo && receive) {
// video has been re-enabled
seekToCurrentPlayback();
}
} else {
log.debug("PlayEngine was null, receiveVideo cannot be modified");
}
}
/** {@inheritDoc} */
public void receiveAudio(boolean receive) {
if (engine != null) {
// check if engine currently receives audio, returns previous value
boolean receiveAudio = engine.receiveAudio(receive);
if (receiveAudio && !receive) {
// send a blank audio packet to reset the player
engine.sendBlankAudio(true);
} else if (!receiveAudio && receive) {
// do a seek
seekToCurrentPlayback();
}
} else {
log.debug("PlayEngine was null, receiveAudio cannot be modified");
}
}
/** {@inheritDoc} */
public void setPlaylistController(IPlaylistController controller) {
this.controller = controller;
}
/** {@inheritDoc} */
public int getItemSize() {
return items.size();
}
/** {@inheritDoc} */
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* {@inheritDoc}
*/
public IPlayItem getCurrentItem() {
return getItem(getCurrentItemIndex());
}
/** {@inheritDoc} */
public IPlayItem getItem(int index) {
read.lock();
try {
return items.get(index);
} catch (IndexOutOfBoundsException e) {
return null;
} finally {
read.unlock();
}
}
/** {@inheritDoc} */
public boolean replace(IPlayItem oldItem, IPlayItem newItem) {
boolean result = false;
read.lock();
try {
int index = items.indexOf(oldItem);
items.remove(index);
items.set(index, newItem);
result = true;
} catch (Exception e) {
} finally {
read.unlock();
}
return result;
}
/**
* Move the current item to the next in list.
*/
private void moveToNext() {
if (controller != null) {
currentItemIndex = controller.nextItem(this, currentItemIndex);
} else {
currentItemIndex = defaultController.nextItem(this, currentItemIndex);
}
}
/**
* Move the current item to the previous in list.
*/
private void moveToPrevious() {
if (controller != null) {
currentItemIndex = controller.previousItem(this, currentItemIndex);
} else {
currentItemIndex = defaultController.previousItem(this, currentItemIndex);
}
}
/**
* {@inheritDoc}
*/
public void onChange(final StreamState state, final Object... changed) {
Notifier notifier = null;
IStreamAwareScopeHandler handler = getStreamAwareHandler();
switch (state) {
case SEEK:
//notifies subscribers on seek
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
//get item being played
IPlayItem item = (IPlayItem) changed[0];
//seek position
int position = (Integer) changed[1];
try {
handler.streamPlayItemSeek(stream, item, position);
} catch (Throwable t) {
log.error("error notify streamPlayItemSeek", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case PAUSED:
//set the paused state
this.setState(StreamState.PAUSED);
//notifies subscribers on pause
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
//get item being played
IPlayItem item = (IPlayItem) changed[0];
//playback position
int position = (Integer) changed[1];
try {
handler.streamPlayItemPause(stream, item, position);
} catch (Throwable t) {
log.error("error notify streamPlayItemPause", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case RESUMED:
//resume playing
this.setState(StreamState.PLAYING);
//notifies subscribers on resume
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
//get item being played
IPlayItem item = (IPlayItem) changed[0];
//playback position
int position = (Integer) changed[1];
try {
handler.streamPlayItemResume(stream, item, position);
} catch (Throwable t) {
log.error("error notify streamPlayItemResume", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case PLAYING:
//notifies subscribers on play
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
//get item being played
IPlayItem item = (IPlayItem) changed[0];
//is it a live broadcast
boolean isLive = (Boolean) changed[1];
try {
handler.streamPlayItemPlay(stream, item, isLive);
} catch (Throwable t) {
log.error("error notify streamPlayItemPlay", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case CLOSED:
//notifies subscribers on close
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
try {
handler.streamSubscriberClose(stream);
} catch (Throwable t) {
log.error("error notify streamSubscriberClose", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case STARTED:
//notifies subscribers on start
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
try {
handler.streamSubscriberStart(stream);
} catch (Throwable t) {
log.error("error notify streamSubscriberStart", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case STOPPED:
//set the stopped state
this.setState(StreamState.STOPPED);
//notifies subscribers on stop
if (handler != null) {
notifier = new Notifier(this, handler) {
public void execute(ISchedulingService service) {
//make sure those notified have the correct connection
Red5.setConnectionLocal(conn);
//get the item that was stopped
IPlayItem item = (IPlayItem) changed[0];
try {
handler.streamPlayItemStop(stream, item);
} catch (Throwable t) {
log.error("error notify streamPlaylistItemStop", t);
} finally {
// clear thread local reference
Red5.setConnectionLocal(null);
}
}
};
}
break;
case END:
//notified by the play engine when the current item reaches the end
nextItem();
break;
default:
//there is no "default" handling
log.warn("Unhandled change: {}", state);
}
if (notifier != null) {
IConnection conn = Red5.getConnectionLocal();
notifier.setConnection(conn);
scheduleOnceJob(notifier);
}
}
/** {@inheritDoc} */
public IPlaylistSubscriberStreamStatistics getStatistics() {
return this;
}
/** {@inheritDoc} */
public long getCreationTime() {
return creationTime;
}
/** {@inheritDoc} */
public int getCurrentTimestamp() {
int lastMessageTs = engine.getLastMessageTimestamp();
if (lastMessageTs >= 0) {
return 0;
}
return lastMessageTs;
}
/** {@inheritDoc} */
public long getBytesSent() {
return bytesSent;
}
/** {@inheritDoc} */
public double getEstimatedBufferFill() {
// check to see if any messages have been sent
int lastMessageTs = engine.getLastMessageTimestamp();
if (lastMessageTs < 0) {
// nothing has been sent yet
return 0.0;
}
// buffer size as requested by the client
final long buffer = getClientBufferDuration();
if (buffer == 0) {
return 100.0;
}
// duration the stream is playing
final long delta = System.currentTimeMillis() - engine.getPlaybackStart();
// expected amount of data present in client buffer
final long buffered = lastMessageTs - delta;
return (buffered * 100.0) / buffer;
}
/** {@inheritDoc} */
public String scheduleOnceJob(IScheduledJob job) {
String jobName = schedulingService.addScheduledOnceJob(10, job);
return jobName;
}
/** {@inheritDoc} */
public String scheduleWithFixedDelay(IScheduledJob job, int interval) {
String jobName = schedulingService.addScheduledJob(interval, job);
jobs.add(jobName);
return jobName;
}
/** {@inheritDoc} */
public void cancelJob(String jobName) {
schedulingService.removeScheduledJob(jobName);
}
/**
* Handles notifications in a separate thread.
*/
public class Notifier implements IScheduledJob {
IPlaylistSubscriberStream stream;
IStreamAwareScopeHandler handler;
IConnection conn;
public Notifier(IPlaylistSubscriberStream stream, IStreamAwareScopeHandler handler) {
log.trace("Notifier - stream: {} handler: {}", stream, handler);
this.stream = stream;
this.handler = handler;
}
public void setConnection(IConnection conn) {
this.conn = conn;
}
public void execute(ISchedulingService service) {
}
}
}