package com.sedmelluq.discord.lavaplayer.remote; import com.sedmelluq.discord.lavaplayer.player.AudioConfiguration; import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import com.sedmelluq.discord.lavaplayer.track.AudioTrackState; import com.sedmelluq.discord.lavaplayer.track.TrackMarker; import com.sedmelluq.discord.lavaplayer.track.TrackMarkerTracker; import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrameBuffer; import com.sedmelluq.discord.lavaplayer.track.playback.AudioTrackExecutor; import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.ENDED; import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.STOPPED; /** * This executor delegates the actual audio processing to a remote node. */ public class RemoteAudioTrackExecutor implements AudioTrackExecutor { private static final Logger log = LoggerFactory.getLogger(RemoteAudioTrackExecutor.class); private static final long NO_SEEK = -1; private static final int BUFFER_DURATION_MS = 3000; private final AudioTrack track; private final AudioConfiguration configuration; private final RemoteNodeManager remoteNodeManager; private final AtomicInteger volumeLevel; private final long executorId; private final AudioFrameBuffer frameBuffer; private final AtomicLong lastFrameTimecode = new AtomicLong(); private final AtomicLong pendingSeek = new AtomicLong(NO_SEEK); private final TrackMarkerTracker markerTracker = new TrackMarkerTracker(); private volatile TrackStateListener activeListener; private volatile boolean hasReceivedData; private volatile boolean hasStarted; private volatile Throwable trackException; /** * @param track Audio track to play * @param configuration Configuration for audio processing * @param remoteNodeManager Manager of remote nodes * @param volumeLevel Mutable volume level */ public RemoteAudioTrackExecutor(AudioTrack track, AudioConfiguration configuration, RemoteNodeManager remoteNodeManager, AtomicInteger volumeLevel) { this.track = track; this.configuration = configuration.copy(); this.remoteNodeManager = remoteNodeManager; this.volumeLevel = volumeLevel; this.executorId = System.nanoTime(); this.frameBuffer = configuration.getFrameBufferFactory().create(BUFFER_DURATION_MS, configuration.getOutputFormat(), null); } /** * @return The unique ID for this executor */ public long getExecutorId() { return executorId; } /** * * @return The configuration to use for processing audio */ public AudioConfiguration getConfiguration() { return configuration; } /** * @return The current volume of the track */ public int getVolume() { return volumeLevel.get(); } /** * @return The track that this executor is playing */ public AudioTrack getTrack() { return track; } /** * @return The position of a seek that has not completed. Value is -1 in case no seeking is in progress. */ public long getPendingSeek() { return pendingSeek.get(); } /** * Clear the current seeking if its position matches the specified position * @param position The position to compare with */ public void clearSeek(long position) { if (position != NO_SEEK) { frameBuffer.setClearOnInsert(); if (pendingSeek.compareAndSet(position, NO_SEEK)) { markerTracker.checkSeekTimecode(position); } } } /** * Send the specified exception as an event to the active state listener. * @param exception Exception to send */ public void dispatchException(FriendlyException exception) { TrackStateListener currentListener = activeListener; ExceptionTools.log(log, exception, track.getIdentifier()); if (currentListener != null) { trackException = exception; currentListener.onTrackException(track, exception); } } /** * Mark that this track has received data from the node. */ public void receivedData() { hasReceivedData = true; } /** * Detach the currently active listener, so no useless reference would be kept and no events would be sent there. */ public void detach() { activeListener = null; markerTracker.trigger(ENDED); } @Override public AudioFrameBuffer getAudioBuffer() { return frameBuffer; } @Override public void execute(TrackStateListener listener) { try { hasStarted = true; activeListener = listener; remoteNodeManager.startPlaying(this); } catch (Throwable throwable) { listener.onTrackException(track, ExceptionTools.wrapUnfriendlyExceptions( "An error occurred when trying to start track remotely.", FriendlyException.Severity.FAULT, throwable)); ExceptionTools.rethrowErrors(throwable); } } @Override public void stop() { frameBuffer.lockBuffer(); frameBuffer.setTerminateOnEmpty(); frameBuffer.clear(); markerTracker.trigger(STOPPED); remoteNodeManager.onTrackEnd(null, track, AudioTrackEndReason.STOPPED); } @Override public long getPosition() { return lastFrameTimecode.get(); } @Override public void setPosition(long timecode) { pendingSeek.set(timecode); } @Override public AudioTrackState getState() { if (hasStarted && activeListener == null) { return AudioTrackState.FINISHED; } else if (!hasReceivedData) { return AudioTrackState.LOADING; } else { return AudioTrackState.PLAYING; } } @Override public void setMarker(TrackMarker marker) { markerTracker.set(marker, getPosition()); } @Override public AudioFrame provide() { AudioFrame frame = frameBuffer.provide(); processProvidedFrame(frame); return frame; } @Override public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { AudioFrame frame = frameBuffer.provide(timeout, unit); processProvidedFrame(frame); return frame; } @Override public boolean provide(MutableAudioFrame targetFrame) { if (frameBuffer.provide(targetFrame)) { processProvidedFrame(targetFrame); return true; } return true; } @Override public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { if (frameBuffer.provide(targetFrame, timeout, unit)) { processProvidedFrame(targetFrame); return true; } return true; } private void processProvidedFrame(AudioFrame frame) { if (frame != null && !frame.isTerminator()) { lastFrameTimecode.set(frame.getTimecode()); if (pendingSeek.get() == NO_SEEK && !frameBuffer.hasClearOnInsert()) { markerTracker.checkPlaybackTimecode(frame.getTimecode()); } } } @Override public boolean failedBeforeLoad() { return trackException != null && !hasReceivedData; } /** * @return The expected timecode of the next frame to receive from the remote node. */ public long getNextInputTimecode() { boolean dataReceived = hasReceivedData; long frameDuration = configuration.getOutputFormat().frameDuration(); if (dataReceived) { Long lastBufferTimecode = frameBuffer.getLastInputTimecode(); if (lastBufferTimecode != null) { return lastBufferTimecode + frameDuration; } } long seekPosition = pendingSeek.get(); if (seekPosition != NO_SEEK) { return seekPosition; } return dataReceived ? lastFrameTimecode.get() + frameDuration : lastFrameTimecode.get(); } @Override public String toString() { return "RemoteExec " + executorId + ", " + track.getIdentifier(); } }