package com.rayo.server.verb; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import javax.media.mscontrol.mediagroup.CodecConstants; import javax.media.mscontrol.mediagroup.FileFormatConstants; import javax.validation.ConstraintValidatorContext; import org.joda.time.Duration; import com.rayo.core.recording.StorageService; import com.rayo.core.verb.Output; import com.rayo.core.verb.Record; import com.rayo.core.verb.RecordCompleteEvent; import com.rayo.core.verb.RecordCompleteEvent.Reason; import com.rayo.core.verb.RecordPauseCommand; import com.rayo.core.verb.RecordResumeCommand; import com.rayo.core.verb.VerbCommand; import com.rayo.core.verb.VerbCompleteEvent; import com.rayo.core.verb.VerbCompleteReason; import com.rayo.server.exception.ExceptionMapper; import com.rayo.server.recording.LocalStore; import com.voxeo.logging.Loggerf; import com.voxeo.moho.MediaException; import com.voxeo.moho.Participant; import com.voxeo.moho.State; import com.voxeo.moho.media.Recording; import com.voxeo.moho.media.record.RecordCommand; import com.voxeo.servlet.xmpp.StanzaError; public class RecordHandler extends AbstractLocalVerbHandler<Record, Participant> { private static final Loggerf log = Loggerf.getLogger(RecordHandler.class); private Recording<Participant> recording; private List<StorageService> storageServices; private LocalStore localStore; private File file; @Override public void start() { if (model.getTo() == null) { try { file = localStore.createRecording(model); if (model.getDuplex() != null && model.getDuplex()) { // Hack to workaround a VCS issue with URIs ( call record has to be file:/// and normal record file:/ URI hackedURI = null; try { hackedURI = new URI("file://" + file.getAbsolutePath()); } catch (URISyntaxException e) {} model.setTo(hackedURI); } else { model.setTo(file.toURI()); } } catch (IOException e) { log.error(e.getMessage(),e); } } RecordCommand command = new RecordCommand(model.getTo()); if (model.getStartBeep() != null) { command.setStartBeep(model.getStartBeep()); } if (model.getStopBeep() != null) { //TODO: https://evolution.voxeo.com/ticket/1506906 } if (model.getStartPaused() != null) { command.setStartInPausedMode(model.getStartPaused()); } if (model.getFinalTimeout() != null) { command.setFinalTimeout(model.getFinalTimeout().getMillis()); } else { command.setFinalTimeout(10000); } if (model.getFormat() != null) { command.setFileFormat(Output.toFileFormat(model.getFormat())); if (command.getFileFormat().equals(FileFormatConstants.RAW)) { command.setAudioCODEC(CodecConstants.LINEAR_16BIT_128K); } } else { command.setFileFormat(FileFormatConstants.WAV); } if (model.getInitialTimeout() != null) { command.setInitialTimeout(model.getInitialTimeout().getMillis()); } else { command.setInitialTimeout(10000); } if (model.getMaxDuration() != null) { command.setMaxDuration(model.getMaxDuration().getMillis()); } command.setSilenceTerminationOn(false); if (model.getDuplex() != null) { command.setDuplex(model.getDuplex()); } else { command.setDuplex(false); } recording = getMediaService().record(command); } @Override public void stop(boolean hangup) { recording.stop(); } @Override public void onCommand(VerbCommand command) { if (command instanceof RecordPauseCommand) { pause(); } else if (command instanceof RecordResumeCommand) { resume(); } } private void pause() { recording.pause(); } private void resume() { recording.resume(); } @Override public boolean isStateValid(ConstraintValidatorContext context) { if (!isReady(participant)) { context.buildConstraintViolationWithTemplate("Call is not ready yet.") .addNode(ExceptionMapper.toString(StanzaError.Condition.RESOURCE_CONSTRAINT)) .addConstraintViolation(); return false; } if (!canManipulateMedia()) { context.buildConstraintViolationWithTemplate("Media operations are not allowed in the current call status.") .addNode(ExceptionMapper.toString(StanzaError.Condition.RESOURCE_CONSTRAINT)) .addConstraintViolation(); return false; } return true; } @State public synchronized void onRecordComplete(com.voxeo.moho.event.RecordCompleteEvent<Participant> event) { if (event.getMediaOperation() != null && !event.getMediaOperation().equals(recording)) { log.debug("Ignoring complete event as it is targeted to a different media operation"); return; } switch(event.getCause()) { case ERROR: case UNKNOWN: log.error("Error while recording conversation"); try { recording.get(); complete(VerbCompleteEvent.Reason.ERROR, event.getErrorText(), event.getDuration()); } catch (Exception e) { if (e.getCause() instanceof MediaException) { complete(VerbCompleteEvent.Reason.ERROR,e.getCause().getMessage(), event.getDuration()); } } break; case TIMEOUT: complete(Reason.TIMEOUT, event.getDuration()); break; case INI_TIMEOUT: complete(Reason.INI_TIMEOUT, event.getDuration()); break; case DISCONNECT: complete(VerbCompleteEvent.Reason.HANGUP, event.getDuration()); break; case CANCEL: case SILENCE: complete(VerbCompleteEvent.Reason.STOP, event.getDuration()); break; } } private void complete(VerbCompleteReason reason, long duration) { complete(reason,null, duration); } private void complete(VerbCompleteReason reason, String errorText, long duration) { RecordCompleteEvent event; long size = 0; // When temp file is null the user has provided a to URL (right now an undocumented feature). In such cases // no storage service policies will be applied. if (file != null) { try { size = file.length(); } catch (Exception e) { log.error(e.getMessage(),e); } URI fileUri = file.toURI(); //TODO: Should we change this and add multiple URIs? Right now only the last URI will make it to the xml for (Object storageService: storageServices) { StorageService ss = (StorageService)storageService; try { URI result = ss.store(file, getParticipant()); if (!result.equals(fileUri)) { log.info("A new recording file is available in: %s", result); model.setTo(result); } } catch (IOException ioe) { event = createRecordCompleteEvent(VerbCompleteEvent.Reason.ERROR); event.setErrorText("Could not store the recording file"); return; } } } event = createRecordCompleteEvent(reason, errorText); event.setDuration(new Duration(duration)); event.setSize(size); complete(event); } private RecordCompleteEvent createRecordCompleteEvent(VerbCompleteReason reason) { return createRecordCompleteEvent(reason); } private RecordCompleteEvent createRecordCompleteEvent(VerbCompleteReason reason, String errorText) { if (errorText != null) { return new RecordCompleteEvent(model, errorText); } else { return new RecordCompleteEvent(model, reason); } } public void setStorageServices(List<StorageService> storageServices) { this.storageServices = storageServices; } public void setLocalStore(LocalStore localStore) { this.localStore = localStore; } }