/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2016, Telestax Inc and individual contributors * by the @authors tag. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.restcomm.media.control.mgcp.pkg.au.pr; import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.log4j.Logger; import org.restcomm.media.control.mgcp.pkg.MgcpEventSubject; import org.restcomm.media.control.mgcp.pkg.au.OperationComplete; import org.restcomm.media.control.mgcp.pkg.au.OperationFailed; import org.restcomm.media.control.mgcp.pkg.au.Playlist; import org.restcomm.media.control.mgcp.pkg.au.ReturnCode; import org.restcomm.media.spi.ResourceUnavailableException; import org.restcomm.media.spi.dtmf.DtmfDetector; import org.restcomm.media.spi.dtmf.DtmfDetectorListener; import org.restcomm.media.spi.listener.TooManyListenersException; import org.restcomm.media.spi.player.Player; import org.restcomm.media.spi.player.PlayerListener; import org.restcomm.media.spi.recorder.Recorder; import org.restcomm.media.spi.recorder.RecorderListener; import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine; /** * @author Henrique Rosa (henrique.rosa@telestax.com) * */ public class PlayRecordFsmImpl extends AbstractStateMachine<PlayRecordFsm, PlayRecordState, PlayRecordEvent, PlayRecordContext> implements PlayRecordFsm { private static final Logger log = Logger.getLogger(PlayRecordFsmImpl.class); // Event Listener private final MgcpEventSubject mgcpEventSubject; // Media Components private final DtmfDetector detector; final DtmfDetectorListener detectorListener; private final Player player; final PlayerListener playerListener; private final Recorder recorder; final RecorderListener recorderListener; // Execution Context private final PlayRecordContext context; public PlayRecordFsmImpl(MgcpEventSubject mgcpEventSubject, Recorder recorder, RecorderListener recorderListener, DtmfDetector detector, DtmfDetectorListener detectorListener, Player player, PlayerListener playerListener, PlayRecordContext context) { super(); // Event Listener this.mgcpEventSubject = mgcpEventSubject; // Media Components this.recorder = recorder; this.recorderListener = recorderListener; this.detector = detector; this.detectorListener = detectorListener; this.player = player; this.playerListener = playerListener; // Execution Context this.context = context; } private void playAnnouncement(String url, long delay) { try { this.player.setInitialDelay(delay); this.player.setURL(url); this.player.activate(); } catch (MalformedURLException e) { log.warn("Could not play malformed segment " + url); context.setReturnCode(ReturnCode.BAD_AUDIO_ID.code()); fire(PlayRecordEvent.FAIL, context); // TODO create transition from PROMPTING to FAILED } catch (ResourceUnavailableException e) { log.warn("Could not play unavailable segment " + url); context.setReturnCode(ReturnCode.BAD_AUDIO_ID.code()); fire(PlayRecordEvent.FAIL, context); // TODO create transition from PROMPTING to FAILED } } private void deleteRecording() { try { Path path = Paths.get(context.getRecordId()); if(Files.exists(path)) { Files.delete(path); } if(log.isTraceEnabled()) { log.trace("Deleted temporary recording file before restart."); } } catch (IOException e) { log.warn("Failed to delete temporary recording file before restart.", e); } } @Override public void enterLoadingPlaylist(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered LOADING PLAYLIST state"); } } @Override public void exitLoadingPlaylist(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited LOADING PLAYLIST state"); } } @Override public void enterPrompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered PROMPTING state"); } final Playlist prompt = context.getInitialPrompt(); try { this.player.addListener(this.playerListener); playAnnouncement(prompt.next(), 0L); } catch (TooManyListenersException e) { log.error("Too many player listeners", e); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onPrompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On PROMPTING state"); } final Playlist prompt = context.getInitialPrompt(); final String next = prompt.next(); if (next.isEmpty()) { // No more announcements to play fire(PlayRecordEvent.PROMPT_END, context); } else { playAnnouncement(next, 10 * 100); } } @Override public void exitPrompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited PROMPTING state"); } this.player.removeListener(this.playerListener); this.player.deactivate(); } @Override public void enterReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered REPROMPTING state"); } final Playlist prompt = context.getReprompt(); try { this.player.addListener(this.playerListener); playAnnouncement(prompt.next(), 0L); } catch (TooManyListenersException e) { log.error("Too many player listeners", e); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On REPROMPTING state"); } final Playlist prompt = context.getReprompt(); final String next = prompt.next(); if (next.isEmpty()) { // No more announcements to play fire(PlayRecordEvent.PROMPT_END, context); } else { playAnnouncement(next, 10 * 100); } } @Override public void exitReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited REPROMPTING state"); } this.player.removeListener(this.playerListener); this.player.deactivate(); } @Override public void enterNoSpeechReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered NO SPEECH REPROMPTING state"); } final Playlist prompt = context.getNoSpeechReprompt(); try { this.player.addListener(this.playerListener); playAnnouncement(prompt.next(), 0L); } catch (TooManyListenersException e) { log.error("Too many player listeners", e); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onNoSpeechReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On NO SPEECH REPROMPTING state"); } final Playlist prompt = context.getNoSpeechReprompt(); final String next = prompt.next(); if (next.isEmpty()) { // No more announcements to play fire(PlayRecordEvent.PROMPT_END, context); } else { playAnnouncement(next, 10 * 100); } } @Override public void exitNoSpeechReprompting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited NO SPEECH REPROMPTING state"); } this.player.removeListener(this.playerListener); this.player.deactivate(); } @Override public void enterCollecting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered COLLECTING state"); } try { // Activate DTMF detector and bind listener this.detector.addListener(this.detectorListener); this.detector.activate(); } catch (TooManyListenersException e) { log.error("Too many DTMF listeners", e); } } @Override public void onCollecting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On COLLECTING state"); } // TODO Make decision when DTMF is collected final char tone = context.getTone(); if(context.getRestartKey() == tone) { fire(PlayRecordEvent.RESTART, context); } else if(context.getReinputKey() == tone) { fire(PlayRecordEvent.REINPUT, context); } else if(context.getEndInputKey() == tone) { fire(PlayRecordEvent.END_RECORD, context); } } @Override public void exitCollecting(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited COLLECTING state"); } // Deactivate DTMF detector and release listener this.detector.removeListener(this.detectorListener); this.detector.deactivate(); } @Override public void enterCollected(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered COLLECTED state"); } } @Override public void enterRecording(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered RECORDING state"); } try { this.recorder.setMaxRecordTime(context.getTotalRecordingLengthTimer() * 1000000L); this.recorder.setPreSpeechTimer(context.getPreSpeechTimer() * 1000000L); this.recorder.setPostSpeechTimer(context.getPostSpeechTimer() * 1000000L); this.recorder.setRecordFile(context.getRecordId(), false); this.recorder.addListener(this.recorderListener); this.recorder.activate(); } catch (IOException e) { log.error("Recording URL cannot be found:" + context.getRecordId(), e); context.setReturnCode(ReturnCode.BAD_AUDIO_ID.code()); fire(PlayRecordEvent.FAIL, context); } catch (TooManyListenersException e) { log.error("Too many recorder listeners."); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onRecording(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("on RECORDING state"); } if (PlayRecordEvent.SPEECH_DETECTED.equals(event)) { // AND && !context.getNonInterruptibleAudio() log.info("SPEECH DETECTED !!!!!!!!"); } } @Override public void exitRecording(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited RECORDING state"); } recorder.deactivate(); recorder.removeListener(this.recorderListener); } @Override public void enterRecorded(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered RECORDED state"); } } @Override public void enterCanceled(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered CANCELED state"); } if(context.isSpeechDetected()) { fire(PlayRecordEvent.SUCCEED, context); } else { context.setReturnCode(ReturnCode.NO_SPEECH.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void exitCanceled(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited CANCELED state"); } } @Override public void enterSucceeding(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered SUCCEEDING state"); } final Playlist playlist = context.getSuccessAnnouncement(); if(playlist.isEmpty()) { fire(PlayRecordEvent.NO_PROMPT, context); } else { fire(PlayRecordEvent.PROMPT, context); } } @Override public void exitSucceeding(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited SUCCEEDING state"); } } @Override public void enterPlayingSuccess(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered PLAYING SUCCESS state"); } final Playlist prompt = context.getSuccessAnnouncement(); try { this.player.addListener(this.playerListener); playAnnouncement(prompt.next(), 0L); } catch (TooManyListenersException e) { log.error("Too many player listeners", e); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onPlayingSuccess(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On PLAYING SUCCESS state"); } final Playlist prompt = context.getSuccessAnnouncement(); final String next = prompt.next(); if (next.isEmpty()) { // No more announcements to play fire(PlayRecordEvent.PROMPT_END, context); } else { playAnnouncement(next, 10 * 100); } } @Override public void exitPlayingSuccess(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited PLAYING SUCCESS state"); } this.player.removeListener(this.playerListener); this.player.deactivate(); } @Override public void enterSucceeded(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if(log.isTraceEnabled()) { log.trace("Entered SUCCEEDED state"); } final OperationComplete oc = new OperationComplete(PlayRecord.SYMBOL, ReturnCode.SUCCESS.code()); oc.setParameter("na", String.valueOf(context.getAttempt())); oc.setParameter("vi", Boolean.FALSE.toString()); oc.setParameter("ri", context.getRecordId()); this.mgcpEventSubject.notify(this.mgcpEventSubject, oc); } @Override public void enterFailing(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered FAILING state"); } if(context.hasMoreAttempts()) { context.newAttempt(); deleteRecording(); fire(event, context); } else { switch (event) { case MAX_DURATION_EXCEEDED: context.setReturnCode(ReturnCode.SPOKE_TOO_LONG.code()); break; case NO_SPEECH: context.setReturnCode(ReturnCode.NO_SPEECH.code()); break; case RESTART: case REINPUT: context.setReturnCode(ReturnCode.MAX_ATTEMPTS_EXCEEDED.code()); break; default: context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); break; } final Playlist playlist = context.getFailureAnnouncement(); if(playlist.isEmpty()) { fire(PlayRecordEvent.NO_PROMPT, context); } else { fire(PlayRecordEvent.PROMPT, context); } } } @Override public void exitFailing(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited FAILING state"); } } @Override public void enterPlayingFailure(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Entered PLAYING FAILURE state"); } final Playlist prompt = context.getFailureAnnouncement(); try { this.player.addListener(this.playerListener); playAnnouncement(prompt.next(), 0L); } catch (TooManyListenersException e) { log.error("Too many player listeners", e); context.setReturnCode(ReturnCode.UNSPECIFIED_FAILURE.code()); fire(PlayRecordEvent.FAIL, context); } } @Override public void onPlayingFailure(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("On PLAYING FAILURE state"); } final Playlist prompt = context.getFailureAnnouncement(); final String next = prompt.next(); if (next.isEmpty()) { // No more announcements to play fire(PlayRecordEvent.PROMPT_END, context); } else { playAnnouncement(next, 10 * 100); } } @Override public void exitPlayingFailure(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if (log.isTraceEnabled()) { log.trace("Exited PLAYING FAILURE state"); } this.player.removeListener(this.playerListener); this.player.deactivate(); } @Override public void enterFailed(PlayRecordState from, PlayRecordState to, PlayRecordEvent event, PlayRecordContext context) { if(log.isTraceEnabled()) { log.trace("Entered FAILED state"); } final OperationFailed of = new OperationFailed(PlayRecord.SYMBOL, context.getReturnCode()); of.setParameter("na", String.valueOf(context.getAttempt())); this.mgcpEventSubject.notify(this.mgcpEventSubject, of); } }