Merge branch 'develop' into f/groupMessages
This commit is contained in:
64
src/main/java/envoy/client/data/audio/AudioPlayer.java
Normal file
64
src/main/java/envoy/client/data/audio/AudioPlayer.java
Normal file
@ -0,0 +1,64 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Plays back audio from a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioPlayer.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioPlayer {
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private Clip clip;
|
||||
|
||||
/**
|
||||
* Initializes the player with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the player with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(Clip.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio play back is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* Plays back an audio clip.
|
||||
*
|
||||
* @param data the data of the clip
|
||||
* @throws EnvoyException if the play back failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void play(byte[] data) throws EnvoyException {
|
||||
try {
|
||||
clip = (Clip) AudioSystem.getLine(info);
|
||||
clip.open(format, data, 0, data.length);
|
||||
clip.start();
|
||||
} catch (final LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot play back audio", e);
|
||||
}
|
||||
}
|
||||
}
|
122
src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
122
src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
@ -0,0 +1,122 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Records audio and exports it as a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioRecorder.java</strong><br>
|
||||
* Created: <strong>02.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioRecorder {
|
||||
|
||||
/**
|
||||
* The default audio format used for recording and play back.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private TargetDataLine line;
|
||||
private Path tempFile;
|
||||
|
||||
/**
|
||||
* Initializes the recorder with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the recorder with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(TargetDataLine.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio recording is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the recorder is active
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isRecording() { return line != null && line.isActive(); }
|
||||
|
||||
/**
|
||||
* Starts the audio recording.
|
||||
*
|
||||
* @throws EnvoyException if starting the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void start() throws EnvoyException {
|
||||
try {
|
||||
|
||||
// Open the line
|
||||
line = (TargetDataLine) AudioSystem.getLine(info);
|
||||
line.open(format);
|
||||
line.start();
|
||||
|
||||
// Prepare temp file
|
||||
tempFile = Files.createTempFile("recording", "wav");
|
||||
|
||||
// Start the recording
|
||||
final var ais = new AudioInputStream(line);
|
||||
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
|
||||
} catch (IOException | LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot record voice", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the recording.
|
||||
*
|
||||
* @return the finished recording
|
||||
* @throws EnvoyException if finishing the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public byte[] finish() throws EnvoyException {
|
||||
try {
|
||||
line.stop();
|
||||
line.close();
|
||||
final byte[] data = Files.readAllBytes(tempFile);
|
||||
Files.delete(tempFile);
|
||||
return data;
|
||||
} catch (final IOException e) {
|
||||
throw new EnvoyException("Cannot save voice recording", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the active recording.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void cancel() {
|
||||
line.stop();
|
||||
line.close();
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
11
src/main/java/envoy/client/data/audio/package-info.java
Normal file
11
src/main/java/envoy/client/data/audio/package-info.java
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Contains classes related to recording and playing back audio clips.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.data.audio;
|
@ -51,16 +51,25 @@ public class Receiver extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
while (true) {
|
||||
try {
|
||||
// Read object length
|
||||
final byte[] lenBytes = new byte[4];
|
||||
in.read(lenBytes);
|
||||
final int len = SerializationUtils.bytesToInt(lenBytes, 0);
|
||||
logger.log(Level.FINEST, "Expecting object of length " + len + ".");
|
||||
|
||||
// Read object into byte array
|
||||
final byte[] objBytes = new byte[len];
|
||||
in.read(objBytes);
|
||||
final byte[] objBytes = new byte[len];
|
||||
final int bytesRead = in.read(objBytes);
|
||||
logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
|
||||
|
||||
// Catch LV encoding errors
|
||||
if (len != bytesRead) {
|
||||
logger.log(Level.WARNING,
|
||||
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
|
||||
continue;
|
||||
}
|
||||
|
||||
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
|
||||
final Object obj = oin.readObject();
|
||||
@ -75,11 +84,12 @@ public class Receiver extends Thread {
|
||||
obj.getClass()));
|
||||
else processor.accept(obj);
|
||||
}
|
||||
} catch (final SocketException e) {
|
||||
// Connection probably closed by client.
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||
}
|
||||
} catch (final SocketException e) {
|
||||
// Connection probably closed by client.
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
49
src/main/java/envoy/client/ui/AudioControl.java
Normal file
49
src/main/java/envoy/client/ui/AudioControl.java
Normal file
@ -0,0 +1,49 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
import envoy.client.data.audio.AudioPlayer;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Enables the play back of audio clips through a button.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioControl.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioControl extends HBox {
|
||||
|
||||
private AudioPlayer player = new AudioPlayer();
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
|
||||
|
||||
/**
|
||||
* Initializes the audio control.
|
||||
*
|
||||
* @param audioData the audio data to play.
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioControl(byte[] audioData) {
|
||||
var button = new Button("Play");
|
||||
button.setOnAction(e -> {
|
||||
try {
|
||||
player.play(audioData);
|
||||
} catch (EnvoyException ex) {
|
||||
logger.log(Level.SEVERE, "Could not play back audio: ", ex);
|
||||
new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
|
||||
}
|
||||
});
|
||||
getChildren().add(button);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.data.audio.AudioRecorder;
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
@ -29,7 +30,9 @@ import envoy.client.ui.listcell.MessageControl;
|
||||
import envoy.client.ui.listcell.MessageListCellFactory;
|
||||
import envoy.data.*;
|
||||
import envoy.event.*;
|
||||
import envoy.data.Attachment.AttachmentType;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
@ -54,6 +57,9 @@ public final class ChatScene implements Restorable {
|
||||
@FXML
|
||||
private Button postButton;
|
||||
|
||||
@FXML
|
||||
private Button voiceButton;
|
||||
|
||||
@FXML
|
||||
private Button settingsButton;
|
||||
|
||||
@ -74,9 +80,11 @@ public final class ChatScene implements Restorable {
|
||||
private WriteProxy writeProxy;
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private boolean postingPermanentlyDisabled = false;
|
||||
|
||||
private Chat currentChat;
|
||||
private Chat currentChat;
|
||||
private AudioRecorder recorder;
|
||||
private boolean recording;
|
||||
private Attachment pendingAttachment;
|
||||
private boolean postingPermanentlyDisabled = false;
|
||||
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
@ -173,6 +181,8 @@ public final class ChatScene implements Restorable {
|
||||
contactLabel.setText(localDB.getUser().getName());
|
||||
MessageControl.setUser(localDB.getUser());
|
||||
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
|
||||
|
||||
recorder = new AudioRecorder();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -206,11 +216,20 @@ public final class ChatScene implements Restorable {
|
||||
logger.log(Level.WARNING, "Could not read current chat.", e);
|
||||
}
|
||||
|
||||
// Discard the pending attachment
|
||||
if (recorder.isRecording()) {
|
||||
recorder.cancel();
|
||||
recording = false;
|
||||
voiceButton.setText("Record Voice Message");
|
||||
}
|
||||
pendingAttachment = null;
|
||||
|
||||
remainingChars.setVisible(true);
|
||||
remainingChars
|
||||
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
|
||||
}
|
||||
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
|
||||
voiceButton.setDisable(!recorder.isSupported());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -235,6 +254,26 @@ public final class ChatScene implements Restorable {
|
||||
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void voiceButtonClicked() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (!recording) {
|
||||
recording = true;
|
||||
Platform.runLater(() -> voiceButton.setText("Recording..."));
|
||||
recorder.start();
|
||||
} else {
|
||||
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
|
||||
recording = false;
|
||||
Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); });
|
||||
}
|
||||
} catch (EnvoyException e) {
|
||||
logger.log(Level.SEVERE, "Could not record audio: ", e);
|
||||
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the text length of the {@code messageTextArea}, adjusts the
|
||||
* {@code remainingChars} label and checks whether to send the message
|
||||
@ -257,11 +296,14 @@ public final class ChatScene implements Restorable {
|
||||
*/
|
||||
@FXML
|
||||
private void checkPostConditions(KeyEvent e) {
|
||||
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown());
|
||||
}
|
||||
|
||||
private void checkPostConditions(boolean sendKeyPressed) {
|
||||
if (!postingPermanentlyDisabled) {
|
||||
if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER
|
||||
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()))
|
||||
postMessage();
|
||||
postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null);
|
||||
if (!postButton.isDisabled() && sendKeyPressed) postMessage();
|
||||
postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null);
|
||||
} else {
|
||||
final var noMoreMessaging = "Go online to send messages";
|
||||
if (!infoLabel.getText().equals(noMoreMessaging))
|
||||
@ -317,11 +359,16 @@ public final class ChatScene implements Restorable {
|
||||
return;
|
||||
}
|
||||
final var text = messageTextArea.getText().strip();
|
||||
if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent.");
|
||||
try {
|
||||
// Create and send message
|
||||
// Creating the message and its metadata
|
||||
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
|
||||
.setText(text);
|
||||
// Setting an attachment, if present
|
||||
if (pendingAttachment != null) {
|
||||
builder.setAttachment(pendingAttachment);
|
||||
pendingAttachment = null;
|
||||
}
|
||||
// Building the final message
|
||||
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
|
||||
: builder.build();
|
||||
|
||||
|
@ -9,6 +9,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.client.ui.AudioControl;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
@ -38,6 +39,20 @@ public class MessageControl extends VBox {
|
||||
public MessageControl(Message message) {
|
||||
// Creating the underlying VBox, the dateLabel and the textLabel
|
||||
super(new Label(dateFormat.format(message.getCreationDate())));
|
||||
|
||||
// Handling message attachment display
|
||||
if (message.hasAttachment()) switch (message.getAttachment().getType()) {
|
||||
case PICTURE:
|
||||
break;
|
||||
case VIDEO:
|
||||
break;
|
||||
case VOICE:
|
||||
getChildren().add(new AudioControl(message.getAttachment().getData()));
|
||||
break;
|
||||
case DOCUMENT:
|
||||
break;
|
||||
}
|
||||
|
||||
final var textLabel = new Label(message.getText());
|
||||
textLabel.setWrapText(true);
|
||||
getChildren().add(textLabel);
|
||||
|
Reference in New Issue
Block a user