// 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();
}
}
}