diff --git a/.editorconfig b/.editorconfig index 1a00f79d3e..e9a2391aee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,5 +7,7 @@ root = true indent_style = tab end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true \ No newline at end of file +# This line causes problems with VSCode and potentially with other editors where all purely +# whitespace lines are trimmed to be empty when saved, causing excessive worthless changes with Git +#trim_trailing_whitespace = true +insert_final_newline = true diff --git a/src/main/java/dev/slimevr/autobone/AutoBone.java b/src/main/java/dev/slimevr/autobone/AutoBone.java index 5f4afe23f6..dfed39e9ec 100644 --- a/src/main/java/dev/slimevr/autobone/AutoBone.java +++ b/src/main/java/dev/slimevr/autobone/AutoBone.java @@ -7,7 +7,7 @@ import com.jme3.math.Vector3f; -import dev.slimevr.poserecorder.PoseFrame; +import dev.slimevr.poserecorder.PoseFrames; import dev.slimevr.poserecorder.TrackerFrame; import dev.slimevr.poserecorder.TrackerFrameData; import io.eiren.util.ann.ThreadSafe; @@ -198,7 +198,7 @@ public float getLengthSum(Map configs) { return length; } - public float getMaxHmdHeight(PoseFrame frames) { + public float getMaxHmdHeight(PoseFrames frames) { float maxHeight = 0f; for(TrackerFrame[] frame : frames) { TrackerFrame hmd = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.HMD); @@ -209,27 +209,27 @@ public float getMaxHmdHeight(PoseFrame frames) { return maxHeight; } - public void processFrames(PoseFrame frames) { + public void processFrames(PoseFrames frames) { processFrames(frames, -1f); } - public void processFrames(PoseFrame frames, Consumer epochCallback) { + public void processFrames(PoseFrames frames, Consumer epochCallback) { processFrames(frames, -1f, epochCallback); } - public void processFrames(PoseFrame frames, float targetHeight) { + public void processFrames(PoseFrames frames, float targetHeight) { processFrames(frames, true, targetHeight); } - public void processFrames(PoseFrame frames, float targetHeight, Consumer epochCallback) { + public void processFrames(PoseFrames frames, float targetHeight, Consumer epochCallback) { processFrames(frames, true, targetHeight, epochCallback); } - public float processFrames(PoseFrame frames, boolean calcInitError, float targetHeight) { + public float processFrames(PoseFrames frames, boolean calcInitError, float targetHeight) { return processFrames(frames, calcInitError, targetHeight, null); } - public float processFrames(PoseFrame frames, boolean calcInitError, float targetHeight, Consumer epochCallback) { + public float processFrames(PoseFrames frames, boolean calcInitError, float targetHeight, Consumer epochCallback) { final int frameCount = frames.getMaxFrameCount(); final SimpleSkeleton skeleton1 = new SimpleSkeleton(configs, staticConfigs); diff --git a/src/main/java/dev/slimevr/gui/AutoBoneWindow.java b/src/main/java/dev/slimevr/gui/AutoBoneWindow.java index 55d2c5b7ec..41aea4752c 100644 --- a/src/main/java/dev/slimevr/gui/AutoBoneWindow.java +++ b/src/main/java/dev/slimevr/gui/AutoBoneWindow.java @@ -25,7 +25,7 @@ import dev.slimevr.autobone.AutoBone; import dev.slimevr.gui.swing.EJBox; -import dev.slimevr.poserecorder.PoseFrame; +import dev.slimevr.poserecorder.PoseFrames; import dev.slimevr.poserecorder.PoseFrameIO; import dev.slimevr.poserecorder.PoseRecorder; @@ -82,7 +82,7 @@ private String getLengthsString() { return configInfo.toString(); } - private void saveRecording(PoseFrame frames) { + private void saveRecording(PoseFrames frames) { if(saveDir.isDirectory() || saveDir.mkdirs()) { File saveRecording; int recordingIndex = 1; @@ -101,15 +101,15 @@ private void saveRecording(PoseFrame frames) { } } - private List> loadRecordings() { - List> recordings = new FastList>(); + private List> loadRecordings() { + List> recordings = new FastList>(); if(loadDir.isDirectory()) { File[] files = loadDir.listFiles(); if(files != null) { for(File file : files) { if(file.isFile() && org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(file.getName(), ".pfr")) { LogManager.log.info("[AutoBone] Detected recording at \"" + file.getPath() + "\", loading frames..."); - PoseFrame frames = PoseFrameIO.readFromFile(file); + PoseFrames frames = PoseFrameIO.readFromFile(file); if(frames == null) { LogManager.log.severe("Reading frames from \"" + file.getPath() + "\" failed..."); @@ -124,7 +124,7 @@ private List> loadRecordings() { return recordings; } - private float processFrames(PoseFrame frames) { + private float processFrames(PoseFrames frames) { autoBone.minDataDistance = server.config.getInt("autobone.minimumDataDistance", autoBone.minDataDistance); autoBone.maxDataDistance = server.config.getInt("autobone.maximumDataDistance", autoBone.maxDataDistance); @@ -172,8 +172,8 @@ public void run() { // 1000 samples at 20 ms per sample is 20 seconds int sampleCount = server.config.getInt("autobone.sampleCount", 1000); long sampleRate = server.config.getLong("autobone.sampleRateMs", 20L); - Future framesFuture = poseRecorder.startFrameRecording(sampleCount, sampleRate); - PoseFrame frames = framesFuture.get(); + Future framesFuture = poseRecorder.startFrameRecording(sampleCount, sampleRate); + PoseFrames frames = framesFuture.get(); LogManager.log.info("[AutoBone] Done recording!"); saveRecordingButton.setEnabled(true); @@ -226,10 +226,10 @@ public void mouseClicked(MouseEvent e) { @Override public void run() { try { - Future framesFuture = poseRecorder.getFramesAsync(); + Future framesFuture = poseRecorder.getFramesAsync(); if(framesFuture != null) { setText("Waiting for Recording..."); - PoseFrame frames = framesFuture.get(); + PoseFrames frames = framesFuture.get(); if(frames.getTrackerCount() <= 0) { throw new IllegalStateException("Recording has no trackers"); @@ -297,15 +297,15 @@ public void mouseClicked(MouseEvent e) { public void run() { try { setText("Load..."); - List> frameRecordings = loadRecordings(); + List> frameRecordings = loadRecordings(); if(!frameRecordings.isEmpty()) { LogManager.log.info("[AutoBone] Done loading frames!"); } else { - Future framesFuture = poseRecorder.getFramesAsync(); + Future framesFuture = poseRecorder.getFramesAsync(); if(framesFuture != null) { setText("Waiting for Recording..."); - PoseFrame frames = framesFuture.get(); + PoseFrames frames = framesFuture.get(); if(frames.getTrackerCount() <= 0) { throw new IllegalStateException("Recording has no trackers"); @@ -331,7 +331,7 @@ public void run() { setText("Processing..."); LogManager.log.info("[AutoBone] Processing frames..."); FastList heightPercentError = new FastList(frameRecordings.size()); - for(Pair recording : frameRecordings) { + for(Pair recording : frameRecordings) { LogManager.log.info("[AutoBone] Processing frames from \"" + recording.getKey() + "\"..."); heightPercentError.add(processFrames(recording.getValue())); diff --git a/src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java b/src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java index 9b8e3bad0a..55a3a316e1 100644 --- a/src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java +++ b/src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java @@ -21,7 +21,7 @@ private PoseFrameIO() { // Do not allow instantiating } - public static boolean writeFrames(DataOutputStream outputStream, PoseFrame frames) { + public static boolean writeFrames(DataOutputStream outputStream, PoseFrames frames) { try { if(frames != null) { outputStream.writeInt(frames.getTrackerCount()); @@ -67,7 +67,7 @@ public static boolean writeFrames(DataOutputStream outputStream, PoseFrame frame return true; } - public static boolean writeToFile(File file, PoseFrame frames) { + public static boolean writeToFile(File file, PoseFrames frames) { try(DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { writeFrames(outputStream, frames); } catch(Exception e) { @@ -78,7 +78,7 @@ public static boolean writeToFile(File file, PoseFrame frames) { return true; } - public static PoseFrame readFrames(DataInputStream inputStream) { + public static PoseFrames readFrames(DataInputStream inputStream) { try { int trackerCount = inputStream.readInt(); @@ -119,7 +119,7 @@ public static PoseFrame readFrames(DataInputStream inputStream) { trackers.add(new PoseFrameTracker(name, trackerFrames)); } - return new PoseFrame(trackers); + return new PoseFrames(trackers); } catch(Exception e) { LogManager.log.severe("Error reading frame from stream", e); } @@ -127,7 +127,7 @@ public static PoseFrame readFrames(DataInputStream inputStream) { return null; } - public static PoseFrame readFromFile(File file) { + public static PoseFrames readFromFile(File file) { try { return readFrames(new DataInputStream(new BufferedInputStream(new FileInputStream(file)))); } catch(Exception e) { diff --git a/src/main/java/dev/slimevr/poserecorder/PoseFrame.java b/src/main/java/dev/slimevr/poserecorder/PoseFrames.java similarity index 91% rename from src/main/java/dev/slimevr/poserecorder/PoseFrame.java rename to src/main/java/dev/slimevr/poserecorder/PoseFrames.java index 0f8cba4ea3..cf4a39643f 100644 --- a/src/main/java/dev/slimevr/poserecorder/PoseFrame.java +++ b/src/main/java/dev/slimevr/poserecorder/PoseFrames.java @@ -7,19 +7,19 @@ import io.eiren.util.collections.FastList; import io.eiren.vr.trackers.Tracker; -public final class PoseFrame implements Iterable { +public final class PoseFrames implements Iterable { private final FastList trackers; - public PoseFrame(FastList trackers) { + public PoseFrames(FastList trackers) { this.trackers = trackers; } - public PoseFrame(int initialCapacity) { + public PoseFrames(int initialCapacity) { this.trackers = new FastList(initialCapacity); } - public PoseFrame() { + public PoseFrames() { this(5); } @@ -103,12 +103,12 @@ public Iterator iterator() { public class PoseFrameIterator implements Iterator { - private final PoseFrame poseFrame; + private final PoseFrames poseFrame; private final TrackerFrame[] trackerFrameBuffer; private int cursor = 0; - public PoseFrameIterator(PoseFrame poseFrame) { + public PoseFrameIterator(PoseFrames poseFrame) { this.poseFrame = poseFrame; trackerFrameBuffer = new TrackerFrame[poseFrame.getTrackerCount()]; } diff --git a/src/main/java/dev/slimevr/poserecorder/PoseRecorder.java b/src/main/java/dev/slimevr/poserecorder/PoseRecorder.java index 9e72e57033..1be97dcad7 100644 --- a/src/main/java/dev/slimevr/poserecorder/PoseRecorder.java +++ b/src/main/java/dev/slimevr/poserecorder/PoseRecorder.java @@ -14,63 +14,81 @@ import io.eiren.vr.trackers.Tracker; public class PoseRecorder { - - protected PoseFrame poseFrame = null; - + + protected PoseFrames poseFrame = null; + protected int numFrames = -1; protected int frameCursor = 0; protected long frameRecordingInterval = 60L; protected long nextFrameTimeMs = -1L; - - protected CompletableFuture currentRecording; - + + protected CompletableFuture currentRecording; + protected final VRServer server; FastList> trackers = new FastList>(); - + public PoseRecorder(VRServer server) { this.server = server; server.addOnTick(this::onTick); } - + @VRServerThread public void onTick() { - if(numFrames > 0) { - PoseFrame poseFrame = this.poseFrame; - List> trackers = this.trackers; - if(poseFrame != null && trackers != null) { - if(frameCursor < numFrames) { - if(System.currentTimeMillis() >= nextFrameTimeMs) { - nextFrameTimeMs = System.currentTimeMillis() + frameRecordingInterval; - - int cursor = frameCursor++; - for(Pair tracker : trackers) { - // Add a frame for each tracker - tracker.getRight().addFrame(cursor, tracker.getLeft()); - } - - // If done, send finished recording - if(frameCursor >= numFrames) { - internalStopRecording(); - } - } - } else { - // If done and hasn't yet, send finished recording - internalStopRecording(); - } + if (numFrames <= 0) { + return; + } + + PoseFrames poseFrame = this.poseFrame; + List> trackers = this.trackers; + if (poseFrame == null || trackers == null) { + return; + } + + if (frameCursor >= numFrames) { + // If done and hasn't yet, send finished recording + stopFrameRecording(); + return; + } + + long curTime = System.currentTimeMillis(); + if (curTime < nextFrameTimeMs) { + return; + } + + nextFrameTimeMs += frameRecordingInterval; + + // To prevent duplicate frames, make sure the frame time is always in the future + if (nextFrameTimeMs <= curTime) { + nextFrameTimeMs = curTime + frameRecordingInterval; + } + + // Make sure it's synchronized since this is the server thread interacting with + // an unknown outside thread controlling this class + synchronized (this) { + // A stopped recording will be accounted for by an empty "trackers" list + int cursor = frameCursor++; + for(Pair tracker : trackers) { + // Add a frame for each tracker + tracker.getRight().addFrame(cursor, tracker.getLeft()); + } + + // If done, send finished recording + if(frameCursor >= numFrames) { + stopFrameRecording(); } } } - - public synchronized Future startFrameRecording(int numFrames, long interval) { - return startFrameRecording(numFrames, interval, server.getAllTrackers()); + + public synchronized Future startFrameRecording(int numFrames, long intervalMs) { + return startFrameRecording(numFrames, intervalMs, server.getAllTrackers()); } - - public synchronized Future startFrameRecording(int numFrames, long interval, List trackers) { + + public synchronized Future startFrameRecording(int numFrames, long intervalMs, List trackers) { if(numFrames < 1) { throw new IllegalArgumentException("numFrames must at least have a value of 1"); } - if(interval < 1) { - throw new IllegalArgumentException("interval must at least have a value of 1"); + if(intervalMs < 1) { + throw new IllegalArgumentException("intervalMs must at least have a value of 1"); } if(trackers == null) { throw new IllegalArgumentException("trackers must not be null"); @@ -81,11 +99,11 @@ public synchronized Future startFrameRecording(int numFrames, long in if(!isReadyToRecord()) { throw new IllegalStateException("PoseRecorder isn't ready to record!"); } - + cancelFrameRecording(); - - poseFrame = new PoseFrame(trackers.size()); - + + poseFrame = new PoseFrames(trackers.size()); + // Update tracker list this.trackers.ensureCapacity(trackers.size()); for(Tracker tracker : trackers) { @@ -93,71 +111,67 @@ public synchronized Future startFrameRecording(int numFrames, long in if(tracker == null || tracker.isComputed()) { continue; } - + // Pair tracker with recording this.trackers.add(Pair.of(tracker, poseFrame.addTracker(tracker, numFrames))); } - + this.frameCursor = 0; this.numFrames = numFrames; - - frameRecordingInterval = interval; + + frameRecordingInterval = intervalMs; nextFrameTimeMs = -1L; - - LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + interval + " ms frame interval"); - - currentRecording = new CompletableFuture(); + + LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + intervalMs + " ms frame interval"); + + currentRecording = new CompletableFuture(); return currentRecording; } - - private void internalStopRecording() { - CompletableFuture currentRecording = this.currentRecording; + + public synchronized void stopFrameRecording() { + CompletableFuture currentRecording = this.currentRecording; if(currentRecording != null && !currentRecording.isDone()) { // Stop the recording, returning the frames recorded currentRecording.complete(poseFrame); } - + numFrames = -1; frameCursor = 0; trackers.clear(); poseFrame = null; } - - public synchronized void stopFrameRecording() { - internalStopRecording(); - } - + public synchronized void cancelFrameRecording() { - CompletableFuture currentRecording = this.currentRecording; + CompletableFuture currentRecording = this.currentRecording; if(currentRecording != null && !currentRecording.isDone()) { // Cancel the current recording and return nothing currentRecording.cancel(true); } - + numFrames = -1; frameCursor = 0; trackers.clear(); poseFrame = null; } - - public boolean isReadyToRecord() { + + public synchronized boolean isReadyToRecord() { return server.getTrackersCount() > 0; } - - public boolean isRecording() { + + public synchronized boolean isRecording() { return numFrames > frameCursor; } - - public boolean hasRecording() { + + public synchronized boolean hasRecording() { return currentRecording != null; } - - public Future getFramesAsync() { + + public synchronized Future getFramesAsync() { return currentRecording; } - - public PoseFrame getFrames() throws ExecutionException, InterruptedException { - CompletableFuture currentRecording = this.currentRecording; + + public synchronized PoseFrames getFrames() throws ExecutionException, InterruptedException { + CompletableFuture currentRecording = this.currentRecording; return currentRecording != null ? currentRecording.get() : null; } } diff --git a/src/main/java/dev/slimevr/posestreamer/BVHFileStream.java b/src/main/java/dev/slimevr/posestreamer/BVHFileStream.java new file mode 100644 index 0000000000..61b4502d8e --- /dev/null +++ b/src/main/java/dev/slimevr/posestreamer/BVHFileStream.java @@ -0,0 +1,185 @@ +package dev.slimevr.posestreamer; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; + +import org.apache.commons.lang3.StringUtils; + +import io.eiren.vr.processor.HumanSkeleton; +import io.eiren.vr.processor.TransformNode; + +public class BVHFileStream extends PoseDataStream { + + private static final int LONG_MAX_VALUE_DIGITS = Long.toString(Long.MAX_VALUE).length(); + private static final float POS_SCALE = 10f; + + private long frameCount = 0; + private final BufferedWriter writer; + + private long frameCountOffset; + + private float[] angleBuf = new float[3]; + private Quaternion rotBuf = new Quaternion(); + + public BVHFileStream(OutputStream outputStream) { + super(outputStream); + writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096); + } + + public BVHFileStream(File file) throws FileNotFoundException { + super(file); + writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096); + } + + public BVHFileStream(String file) throws FileNotFoundException { + super(file); + writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096); + } + + private String getBufferedFrameCount(long frameCount) { + String frameString = Long.toString(frameCount); + int bufferCount = LONG_MAX_VALUE_DIGITS - frameString.length(); + + return bufferCount > 0 ? frameString + StringUtils.repeat(' ', bufferCount) : frameString; + } + + private void writeTransformNodeHierarchy(TransformNode node) throws IOException { + writeTransformNodeHierarchy(node, 0); + } + + private void writeTransformNodeHierarchy(TransformNode node, int level) throws IOException { + String indentLevel = StringUtils.repeat("\t", level); + String nextIndentLevel = indentLevel + "\t"; + + // Handle ends + if (node.children.isEmpty()) { + writer.write(indentLevel + "End Site\n"); + } else { + writer.write((level > 0 ? indentLevel + "JOINT " : "ROOT ") + node.getName() + "\n"); + } + writer.write(indentLevel + "{\n"); + + if (level > 0) { + Vector3f offset = node.localTransform.getTranslation(); + writer.write(nextIndentLevel + "OFFSET " + Float.toString(offset.getX() * POS_SCALE) + " " + Float.toString(offset.getY() * POS_SCALE) + " " + Float.toString(offset.getZ() * POS_SCALE) + "\n"); + } else { + writer.write(nextIndentLevel + "OFFSET 0.0 0.0 0.0\n"); + } + + // Handle ends + if (!node.children.isEmpty()) { + // Only give position for root + if (level > 0) { + writer.write(nextIndentLevel + "CHANNELS 3 Zrotation Xrotation Yrotation\n"); + } else { + writer.write(nextIndentLevel + "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n"); + } + + for (TransformNode childNode : node.children) { + writeTransformNodeHierarchy(childNode, level + 1); + } + } + + writer.write(indentLevel + "}\n"); + } + + @Override + public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException { + if (skeleton == null) { + throw new NullPointerException("skeleton must not be null"); + } + if (streamer == null) { + throw new NullPointerException("streamer must not be null"); + } + + writer.write("HIERARCHY\n"); + writeTransformNodeHierarchy(skeleton.getRootNode()); + + writer.write("MOTION\n"); + writer.write("Frames: "); + + // Get frame offset for finishing writing the file + if (outputStream instanceof FileOutputStream) { + FileOutputStream fileOutputStream = (FileOutputStream)outputStream; + // Flush buffer to get proper offset + writer.flush(); + frameCountOffset = fileOutputStream.getChannel().position(); + } + + writer.write(getBufferedFrameCount(frameCount) + "\n"); + + // Frame time in seconds + writer.write("Frame Time: " + (streamer.frameRecordingInterval / 1000d) + "\n"); + } + + private void writeTransformHierarchyRotation(TransformNode node, Quaternion inverseRootRot) throws IOException { + rotBuf = node.localTransform.getRotation(rotBuf); + + // Adjust to local rotation + if (inverseRootRot != null) { + rotBuf = inverseRootRot.mult(rotBuf, rotBuf); + } + + angleBuf = rotBuf.toAngles(angleBuf); + writer.write(Float.toString((float)Math.toDegrees(angleBuf[2])) + " " + Float.toString((float)Math.toDegrees(angleBuf[0])) + " " + Float.toString((float)Math.toDegrees(angleBuf[1]))); + + // Get inverse rotation for child local rotations + Quaternion inverseRot = node.localTransform.getRotation().inverse(); + for (TransformNode childNode : node.children) { + if (childNode.children.isEmpty()) { + // If it's an end node, skip + continue; + } + + // Add spacing + writer.write(" "); + writeTransformHierarchyRotation(childNode, inverseRot); + } + } + + @Override + public void writeFrame(HumanSkeleton skeleton) throws IOException { + if (skeleton == null) { + throw new NullPointerException("skeleton must not be null"); + } + + TransformNode root = skeleton.getRootNode(); + Vector3f rootPos = root.localTransform.getTranslation(); + + // Write root position + writer.write(Float.toString(rootPos.getX() * POS_SCALE) + " " + Float.toString(rootPos.getY() * POS_SCALE) + " " + Float.toString(rootPos.getZ() * POS_SCALE) + " "); + writeTransformHierarchyRotation(root, null); + + writer.newLine(); + + frameCount++; + } + + @Override + public void writeFooter(HumanSkeleton skeleton) throws IOException { + // Write the final frame count for files + if (outputStream instanceof FileOutputStream) { + FileOutputStream fileOutputStream = (FileOutputStream)outputStream; + // Flush before anything else + writer.flush(); + // Seek to the count offset + fileOutputStream.getChannel().position(frameCountOffset); + // Overwrite the count with a new value + writer.write(Long.toString(frameCount)); + } + } + + @Override + public void close() throws IOException { + writer.close(); + super.close(); + } +} diff --git a/src/main/java/dev/slimevr/posestreamer/PoseDataStream.java b/src/main/java/dev/slimevr/posestreamer/PoseDataStream.java new file mode 100644 index 0000000000..dc23038b69 --- /dev/null +++ b/src/main/java/dev/slimevr/posestreamer/PoseDataStream.java @@ -0,0 +1,45 @@ +package dev.slimevr.posestreamer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import io.eiren.vr.processor.HumanSkeleton; + +public abstract class PoseDataStream implements AutoCloseable { + + protected boolean closed = false; + protected final OutputStream outputStream; + + protected PoseDataStream(OutputStream outputStream) { + this.outputStream = outputStream; + } + + protected PoseDataStream(File file) throws FileNotFoundException { + this(new FileOutputStream(file)); + } + + protected PoseDataStream(String file) throws FileNotFoundException { + this(new FileOutputStream(file)); + } + + public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException { + } + + abstract void writeFrame(HumanSkeleton skeleton) throws IOException; + + public void writeFooter(HumanSkeleton skeleton) throws IOException { + } + + public boolean isClosed() { + return closed; + } + + @Override + public void close() throws IOException { + outputStream.close(); + closed = true; + } +} diff --git a/src/main/java/dev/slimevr/posestreamer/PoseStreamer.java b/src/main/java/dev/slimevr/posestreamer/PoseStreamer.java new file mode 100644 index 0000000000..e339be4e95 --- /dev/null +++ b/src/main/java/dev/slimevr/posestreamer/PoseStreamer.java @@ -0,0 +1,110 @@ +package dev.slimevr.posestreamer; + +import java.io.IOException; + +import io.eiren.util.ann.VRServerThread; +import io.eiren.util.logging.LogManager; +import io.eiren.vr.VRServer; +import io.eiren.vr.processor.HumanSkeleton; + +public class PoseStreamer { + + protected long frameRecordingInterval = 60L; + protected long nextFrameTimeMs = -1L; + + private HumanSkeleton skeleton; + private PoseDataStream poseFileStream; + + protected final VRServer server; + + public PoseStreamer(VRServer server) { + this.server = server; + + // Register callbacks/events + server.addSkeletonUpdatedCallback(this::onSkeletonUpdated); + server.addOnTick(this::onTick); + } + + @VRServerThread + public void onSkeletonUpdated(HumanSkeleton skeleton) { + this.skeleton = skeleton; + } + + @VRServerThread + public void onTick() { + PoseDataStream poseFileStream = this.poseFileStream; + if (poseFileStream == null) { + return; + } + + HumanSkeleton skeleton = this.skeleton; + if (skeleton == null) { + return; + } + + long curTime = System.currentTimeMillis(); + if (curTime < nextFrameTimeMs) { + return; + } + + nextFrameTimeMs += frameRecordingInterval; + + // To prevent duplicate frames, make sure the frame time is always in the future + if (nextFrameTimeMs <= curTime) { + nextFrameTimeMs = curTime + frameRecordingInterval; + } + + // Make sure it's synchronized since this is the server thread interacting with + // an unknown outside thread controlling this class + synchronized (this) { + // Make sure the stream is open before trying to write + if (poseFileStream.isClosed()) { + return; + } + + try { + poseFileStream.writeFrame(skeleton); + } catch (Exception e) { + // Handle any exceptions without crashing the program + LogManager.log.severe("[PoseStreamer] Exception while saving frame", e); + } + } + } + + public synchronized void setFrameInterval(long intervalMs) { + if(intervalMs < 1) { + throw new IllegalArgumentException("intervalMs must at least have a value of 1"); + } + + this.frameRecordingInterval = intervalMs; + } + + public synchronized long getFrameInterval() { + return frameRecordingInterval; + } + + public synchronized void setOutput(PoseDataStream poseFileStream) throws IOException { + poseFileStream.writeHeader(skeleton, this); + this.poseFileStream = poseFileStream; + nextFrameTimeMs = -1L; // Reset the frame timing + } + + public synchronized void setOutput(PoseDataStream poseFileStream, long intervalMs) throws IOException { + setFrameInterval(intervalMs); + setOutput(poseFileStream); + } + + public synchronized PoseDataStream getOutput() { + return poseFileStream; + } + + public synchronized void closeOutput() throws IOException { + PoseDataStream poseFileStream = this.poseFileStream; + + if (poseFileStream != null) { + poseFileStream.writeFooter(skeleton); + poseFileStream.close(); + this.poseFileStream = null; + } + } +}