// Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. using Meta.XR.Movement.Playback; using Meta.XR.Movement.Retargeting; using System; using System.IO; using Unity.Collections; using UnityEngine; using static Meta.XR.Movement.MSDKUtility; namespace Meta.XR.Movement.Recording { /// /// Reads retargeter data from file to apply to a character. /// public class SequenceFileReader : IPlaybackBehaviour { /// public int SnapshotIndex => _playbackManager.SnapshotIndex; /// public int NumSnapshots => _playbackManager.NumSnapshots; /// public bool HasOpenedFileForPlayback => _playbackManager.HasActivePlaybackFile; /// public float BandwidthKbps => _bandwidthRecorder.BandwidthKbps; private BandwidthRecorder _bandwidthRecorder = new BandwidthRecorder(); /// public bool UserActivelyScrubbing { get => _activelyScrubbing; set { _activelyScrubbing = value; } } /// /// Gets the current body pose data from the tracker. /// public NativeArray BodyPose => _deserSourcePose; /// /// Gets whether the playback is currently active and not paused. /// public bool IsPlaying => _playbackManager.HasActivePlaybackFile && !_pauseState; /// /// Gets the current playback time in seconds relative to the start time. /// public float PlaybackTime => _playbackManager.NetworkTime - (float)_playbackManager.StartNetworkTime; private UInt64 _playbackHandle; private SequencePlaybackManager _playbackManager = new SequencePlaybackManager(); private NativeArray _deserSourcePose; private SerializationCompressionType _receivedCompressionType; private int _numSourceJoints; private int[] _sourceParentIndices; private bool _activelyScrubbing; private bool _pauseState = false; ~SequenceFileReader() { CleanUp(); } /// /// Initializes the sequence file reader with the specified retargeting handle. /// /// The retargeting handle to use for playback. public void Init(UInt64 handle) { _playbackHandle = handle; GetSkeletonInfo(_playbackHandle, SkeletonType.SourceSkeleton, out var sourceSkeletonInfo); _numSourceJoints = sourceSkeletonInfo.JointCount; var nativeSourceParentIndices = new NativeArray(_numSourceJoints, Allocator.Temp, NativeArrayOptions.UninitializedMemory); GetParentJointIndexesByRef(_playbackHandle, SkeletonType.SourceSkeleton, ref nativeSourceParentIndices); _sourceParentIndices = nativeSourceParentIndices.ToArray(); } /// public bool Seek(int snapshotIndex) { return _playbackManager.Seek( _playbackHandle, snapshotIndex, ProcessSnapshotGetNetworkTimeDelegate, DeserializeData); } /// public bool PlayBackRecording(string playbackAssetPath) { _playbackManager.ClosePlaybackFileIfOpen(); var finalPath = Path.Combine(Application.dataPath, playbackAssetPath); _playbackManager.OpenFileForPlayback(_playbackHandle, finalPath); if (_playbackManager.HasActivePlaybackFile) { MSDKUtility.ResetInterpolators(_playbackHandle); _playbackManager.ResetTimestampsToStart(); _pauseState = false; } return _playbackManager.HasActivePlaybackFile; } /// public void SetPauseState(bool pauseState) { _pauseState = pauseState; } /// public void ClosePlaybackFile() { _playbackManager.ClosePlaybackFileIfOpen(); } /// /// Plays the next frame from the sequence file, updating the pose data. /// This method handles time progression, looping, and interpolation. /// public void PlayNextFrame() { if (_activelyScrubbing) { return; } if (_pauseState) { return; } _playbackManager.NetworkTime += Time.deltaTime; if (_playbackManager.ReadAllSnapshots) { _playbackManager.ResetTimestampsToStart(); _playbackManager.RestartSnapshotReading(); ResetInterpolators(_playbackHandle); } if (_playbackManager.NetworkTime < _playbackManager.LastTimeStamp && _playbackManager.NetworkTime > 0.0f) { return; } byte[] snapshotBytes = _playbackManager.ReadNextSnapshotBytes(); if (snapshotBytes != null) { float readTimestamp = ProcessSnapshotGetNetworkTimeDelegate(snapshotBytes); _playbackManager.LastTimeStamp = (float)readTimestamp; } } /// /// Processes snapshot bytes and returns the network time. /// This method is used as a delegate for snapshot processing during playback. /// /// The snapshot bytes to process. /// The network time of the processed snapshot. public float ProcessSnapshotGetNetworkTimeDelegate( byte[] snapshotBytes) { return _playbackManager.ProcessSnapshotBytesAndGetNetworkTime( snapshotBytes, _activelyScrubbing, DeserializeData, LerpReceivedData, ProcessReceivedData); } private void LerpReceivedData() { GetInterpolatedSkeleton( _playbackHandle, SkeletonType.SourceSkeleton, ref _deserSourcePose, _playbackManager.NetworkTime); } private void ProcessReceivedData() { if (CompressionUsesJointLengths(_receivedCompressionType)) { SkeletonUtilities.ConvertLocalPosesToAbsolute(_sourceParentIndices, _deserSourcePose); } } private bool DeserializeData(byte[] bytes) { AllocateDeserializationArrays(); var nativeBytes = new NativeArray(bytes.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory); for (int i = 0; i < bytes.Length; i++) { nativeBytes[i] = bytes[i]; } // Create dummy arguments for deserialized data that we don't care about. var emptyBody = new NativeArray(1, Allocator.Temp); var emptyFace = new NativeArray(1, Allocator.Temp); var frameData = new FrameData(); if (!DeserializeSkeletonAndFace( _playbackHandle, nativeBytes, out var timestamp, out _receivedCompressionType, out var ack, ref emptyBody, ref emptyFace, ref _deserSourcePose, ref frameData)) { Debug.LogError("Data deserialized is invalid!"); return false; } return true; } private void AllocateDeserializationArrays() { if (!_deserSourcePose.IsCreated) { _deserSourcePose = new NativeArray(_numSourceJoints, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); } } private void CleanUp() { if (_deserSourcePose.IsCreated) { _deserSourcePose.Dispose(); } _playbackManager.ClosePlaybackFileIfOpen(); } } }