diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3f5a374771d79b22857a7c702efb05664a6979c..991a3f1f1ec396cb636bfa7e55bce43e8d5e579b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## 1.0.1 - 2025-06-24
 
+### Added
+
+- Public property `isActivated` in `AprilTagDetector`
+- Public method `AddTag`, `RemoveTag`, and `SetTagSizes` in `AprilTagDetector`
+- Tooltip for `AprilTagDetector` serialized fields
+- `DebugLog` utility based on custom scripting symbols
 
 ## 1.0.0 - 2025-06-17
 
diff --git a/README.md b/README.md
index df2b8e9f81c52e2106065665af3e258e505ddf88..bbc7ab9975a823572f84544da87d5cee61f71475 100644
--- a/README.md
+++ b/README.md
@@ -23,12 +23,18 @@ This package is available in the `HYPER` scoped registry.
 
 Please follow [this snippet] to add the registry to your project.
 
+## How to use
+
+### Enable debug messages
+
+Add `HYPER_PCA_APRILTAG_DEBUG_LOG` to **Project Settings** > **Player** > **Script Compilation** > **Scripting Define Symbols**. 
+
+## Attribution
+
+- Correct Blips by CogFireStudios -- https://freesound.org/s/531510/ -- License: Creative Commons 0
+
 [jp.keijiro.apriltag]: https://github.com/keijiro/jp.keijiro.apriltag
 [LICENSE.BSD-2-Clause]: https://git-st.inf.tu-dresden.de/hyper/pca-apriltag/-/blob/main/LICENSE.BSD-2-Clause
 [Unity-PassthroughCameraApiSamples]: https://github.com/oculus-samples/Unity-PassthroughCameraApiSamples/tree/main/Assets/PassthroughCameraApiSamples
 [LICENSE.MIT]: https://git-st.inf.tu-dresden.de/hyper/pca-apriltag/-/blob/main/LICENSE.MIT
 [this snippet]: https://git-st.inf.tu-dresden.de/hyper/core/-/snippets/14
-
-## Attribution
-
-- Correct Blips by CogFireStudios -- https://freesound.org/s/531510/ -- License: Creative Commons 0
diff --git a/Runtime/Scripts/AprilTagDetector.cs b/Runtime/Scripts/AprilTagDetector.cs
index eb4efff43d7a3c0a222595074241a031ea593093..f52ebac63cfe8c9859d344bf5e4e31cabe0a863c 100644
--- a/Runtime/Scripts/AprilTagDetector.cs
+++ b/Runtime/Scripts/AprilTagDetector.cs
@@ -10,18 +10,46 @@ namespace Hyper.PCAAprilTag
 {
     public class AprilTagDetector : MonoBehaviour
     {
+        /// <summary>
+        /// Indicates whether the AprilTag detector is currently activated.
+        /// </summary>
+        public bool isActivated { get; private set; }
+
         public static event Action<IEnumerable<TagPoseSize>> OnTagsDetected;
 
-        [SerializeField] private bool isContinuous;
-        [SerializeField] private List<IdSizePair> tagSizes = new();
-        [SerializeField] private int frameDelay = 7;
-        [SerializeField] private int numAveragedPoses = 10;
-        [SerializeField] private float maxLinearSpeed = 0.1f;     // meters per second
-        [SerializeField] private float maxAngularSpeed = 10f;     // degrees per second
-        [SerializeField] private float timeout = 2f;
-        [SerializeField] private int decimation = 4;
+        [SerializeField, Tooltip("If true, the detector will continuously search for tags until Deactivate() is called.")]
+        private bool isContinuous;
+
+        [SerializeField, Tooltip("If true, plays a sound when tags are detected.")]
+        private bool playSuccessSound = true;
+
+        [SerializeField, Tooltip("List of tag IDs and their sizes. " +
+                                 "Only tags listed here will be processed by the AprilTag detector.")]
+        private List<IdSizePair> tagSizes = new();
+
+        [SerializeField, Tooltip("Use the past frameDelay camera poses when detecting tags " +
+                                 "to compensate for WebCamTexture latency.")]
+        private int frameDelay = 7;
+
+        [SerializeField, Tooltip("Number of poses to average for each detected tag. " +
+                                 "Higher values can improve stability but may introduce latency.")]
+        private int numAveragedPoses = 10;
+
+        [SerializeField, Tooltip("Maximum linear speed of the headset before detection is skipped. " +
+                                 "This is to avoid bad detections when the headset is moving too fast.")]
+        private float maxLinearSpeed = 0.1f;     // meters per second
+
+        [SerializeField, Tooltip("Maximum angular speed of the headset before detection is skipped. " +
+                                 "This is to avoid bad detections when the headset is rotating too fast.")]
+        private float maxAngularSpeed = 10f;     // degrees per second
+
+        // [SerializeField] private float timeout = 2f; // TODO
+
+        [SerializeField, Tooltip("Decimation factor for the image processing. " +
+                                 "Higher values reduce the resolution of the input image, " +
+                                 "which can speed up processing but may reduce accuracy.")]
+        private int decimation = 1;
 
-        private bool _isActivated;
         private float _verticalFOV;
         private TagDetector _detector;
         private WebCamTextureManager _webCamTextureManager;
@@ -30,14 +58,13 @@ namespace Hyper.PCAAprilTag
         private Thread _detectorProcessImageThread;
         private List<TagPoseSize> _detectedTags = new();
         private Lifo<Pose> _cameraPoses;
-        private Dictionary<int, Lifo<TagPoseSize>> _tagPoseSizeDict = new();
+        private readonly Dictionary<int, Lifo<TagPoseSize>> _tagPoseSizeDict = new();
         private AudioSource _doneAudioSource;
 
-        private Vector3 lastPosition;
-        private Quaternion lastRotation;
-        private float lastTime;
-
-        private Transform headset;
+        private Transform _headset;
+        private Vector3 _lastPosition;
+        private Quaternion _lastRotation;
+        private float _lastTime;
 
         private void Awake()
         {
@@ -65,10 +92,10 @@ namespace Hyper.PCAAprilTag
             var cameraIntrinsics = PassthroughCameraUtils.GetCameraIntrinsics(CameraEye);
             _verticalFOV = 2 * Mathf.Atan2(cameraIntrinsics.Resolution.y, 2 * cameraIntrinsics.FocalLength.y);
 
-            headset = Camera.main!.transform;
-            lastPosition = headset.position;
-            lastRotation = headset.rotation;
-            lastTime = Time.time;
+            _headset = Camera.main!.transform;
+            _lastPosition = _headset.position;
+            _lastRotation = _headset.rotation;
+            _lastTime = Time.time;
         }
 
         private void OnDestroy()
@@ -85,6 +112,7 @@ namespace Hyper.PCAAprilTag
 
         private void LateUpdate()
         {
+            // TODO. This might be a problem if the Camera Rig is moved.
             _cameraPoses.Push(PassthroughCameraUtils.GetCameraPoseInWorld(CameraEye));
 
             if (_detectedTags.Any())
@@ -103,44 +131,44 @@ namespace Hyper.PCAAprilTag
                 if (!isContinuous && _tagPoseSizeDict.First().Value.Count >= numAveragedPoses)
                 {
                     Deactivate();
-                    _doneAudioSource.Play();
+                    if (playSuccessSound) _doneAudioSource.Play();
                     _tagPoseSizeDict.Clear();
                 }
 
                 OnTagsDetected?.Invoke(AvgDetectedTags());
             }
 
-            if (!_isActivated || !_webCamTextureManager.WebCamTexture ||
+            if (!isActivated || !_webCamTextureManager.WebCamTexture ||
                 _detectorProcessImageThread is { IsAlive: true }) return;
 
             var currentTime = Time.time;
-            var deltaTime = currentTime - lastTime;
+            var deltaTime = currentTime - _lastTime;
 
             if (deltaTime > 0f)
             {
                 // Linear velocity
-                float linearSpeed = Vector3.Distance(headset.position, lastPosition) / deltaTime;
+                var linearSpeed = Vector3.Distance(_headset.position, _lastPosition) / deltaTime;
 
                 // Angular velocity
-                Quaternion deltaRotation = headset.rotation * Quaternion.Inverse(lastRotation);
+                var deltaRotation = _headset.rotation * Quaternion.Inverse(_lastRotation);
                 deltaRotation.ToAngleAxis(out float angleInDegrees, out _);
-                float angularSpeed = angleInDegrees / deltaTime;
+                var angularSpeed = angleInDegrees / deltaTime;
 
                 // Update
-                lastPosition = headset.position;
-                lastRotation = headset.rotation;
-                lastTime = currentTime;
+                _lastPosition = _headset.position;
+                _lastRotation = _headset.rotation;
+                _lastTime = currentTime;
 
                 // Check thresholds
                 if (linearSpeed > maxLinearSpeed)
                 {
-                    Debug.LogWarning($"[Camera] Headset moving too fast (linear): {linearSpeed:F2} m/s");
+                    DebugLog.Log(LogType.Warning, $"[Camera] Headset moving too fast (linear): {linearSpeed:F2} m/s");
                     return;
                 }
 
                 if (angularSpeed > maxAngularSpeed)
                 {
-                    Debug.LogWarning($"[Camera] Headset rotating too fast: {angularSpeed:F2} deg/s");
+                    DebugLog.Log(LogType.Warning, $"[Camera] Headset rotating too fast: {angularSpeed:F2} deg/s");
                     return;
                 }
             }
@@ -160,28 +188,28 @@ namespace Hyper.PCAAprilTag
             _detectorProcessImageThread.Start();
         }
 
-        private void ProcessImage(Color32[] image, Matrix4x4 cameraToWorldMatrix)
-        {
-            _detector.ProcessImage(image, _verticalFOV, _tagSizes, cameraToWorldMatrix);
-            _detectedTags = _detector.GetDetectedTagsCopy();
-        }
-
+        /// <summary>
+        /// Activate the AprilTag detector and start the WebCamTexture.
+        /// </summary>
         public void Activate()
         {
-            if (_isActivated) return;
+            if (isActivated) return;
 
-            _isActivated = true;
+            isActivated = true;
             _webCamTextureManager.WebCamTexture.Play();
         }
 
+        /// <summary>
+        /// Deactivate the AprilTag detector and stop the WebCamTexture.
+        /// </summary>
         public void Deactivate()
         {
-            _isActivated = false;
+            isActivated = false;
             _webCamTextureManager.WebCamTexture.Stop();
         }
 
         /// <summary>
-        /// Method to wait for the camera to be ready and then activate the AprilTag detector.
+        /// Wait for the camera to be ready and then activate the AprilTag detector.
         /// Useful if AprilTag detection should run when the application starts.
         /// </summary>
         public void WaitForCamReadyAndActivate() =>
@@ -199,6 +227,72 @@ namespace Hyper.PCAAprilTag
             }
         }
 
+        /// <summary>
+        /// Add a new tag with its ID and size.
+        /// </summary>
+        /// <param name="id">Tag ID.</param>
+        /// <param name="size">Tag size.</param>
+        /// <returns>Success boolean.</returns>
+        public bool AddTag(int id, float size)
+        {
+            if (tagSizes.Any(pair => pair.id == id))
+            {
+                DebugLog.Log(LogType.Warning, $"Tag with ID {id} already exists.");
+                return false;
+            }
+
+            tagSizes.Add(new IdSizePair(id, size));
+            _tagSizes = TagSizesDictionary();
+            return true;
+        }
+
+        /// <summary>
+        /// Remove a tag by its ID.
+        /// </summary>
+        /// <param name="id">Tag ID.</param>
+        /// <returns></returns>
+        public bool RemoveTag(int id)
+        {
+            if (tagSizes.Any(pair => pair.id == id))
+            {
+                tagSizes.Remove(tagSizes.First(pair => pair.id == id));
+                _tagSizes = TagSizesDictionary();
+                return true;
+            }
+
+            DebugLog.Log(LogType.Warning, $"Tag with ID {id} does not exist.");
+            return false;
+        }
+
+        /// <summary>
+        /// Set the tagSizes list.
+        /// </summary>
+        /// <param name="ids">Array of tag IDs.</param>
+        /// <param name="sizes">Array of tag sizes.</param>
+        /// <returns>Success boolean.</returns>
+        public bool SetTagSizes(int[] ids, float[] sizes)
+        {
+            if (ids.Length != sizes.Length)
+            {
+                DebugLog.Log(LogType.Error, "IDs and sizes arrays must have the same length.");
+                return false;
+            }
+
+            tagSizes.Clear();
+            for (var i = 0; i < ids.Length; i++)
+            {
+                tagSizes.Add(new IdSizePair(ids[i], sizes[i]));
+            }
+            _tagSizes = TagSizesDictionary();
+            return true;
+        }
+
+        private void ProcessImage(Color32[] image, Matrix4x4 cameraToWorldMatrix)
+        {
+            _detector.ProcessImage(image, _verticalFOV, _tagSizes, cameraToWorldMatrix);
+            _detectedTags = _detector.GetDetectedTagsCopy();
+        }
+
         private Dictionary<int, float> TagSizesDictionary()
         {
             var dict = new Dictionary<int, float>();
diff --git a/Runtime/Scripts/DebugLog.cs b/Runtime/Scripts/DebugLog.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f63b85faa700955c50b56c9a8c76c3ccd1eb25b4
--- /dev/null
+++ b/Runtime/Scripts/DebugLog.cs
@@ -0,0 +1,34 @@
+using Debug = UnityEngine.Debug;
+using System;
+using System.Diagnostics;
+using UnityEngine;
+
+namespace Hyper.PCAAprilTag
+{
+    /// <summary>
+    /// A utility class for logging debug messages.
+    /// To enable, add "HYPER_PCA_APRILTAG_DEBUG_LOG"
+    /// to Project Settings > Player > Script Compilation > Scripting Define Symbols
+    /// </summary>
+    public static class DebugLog
+    {
+        [Conditional("HYPER_PCA_APRILTAG_DEBUG_LOG")]
+        public static void Log(LogType mType, string message)
+        {
+            switch (mType)
+            {
+                case LogType.Error:
+                    Debug.LogError(message);
+                    break;
+                case LogType.Warning:
+                    Debug.LogWarning(message);
+                    break;
+                case LogType.Log:
+                    Debug.Log(message);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(mType), mType, null);
+            }
+        }
+    }
+}
diff --git a/Runtime/Scripts/DebugLog.cs.meta b/Runtime/Scripts/DebugLog.cs.meta
new file mode 100644
index 0000000000000000000000000000000000000000..99eabe4531c930f120d44ea4ea9b5c9aa0fca919
--- /dev/null
+++ b/Runtime/Scripts/DebugLog.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 28ff872694bba47d899df394223ec8d0
\ No newline at end of file
diff --git a/Runtime/Scripts/IdSizePair.cs b/Runtime/Scripts/IdSizePair.cs
index 892c12bab2e3373b63bcad951c50f4f144e79fc2..73a374fc2aee1234a443e9ba51b12b29286e563b 100644
--- a/Runtime/Scripts/IdSizePair.cs
+++ b/Runtime/Scripts/IdSizePair.cs
@@ -3,9 +3,30 @@ using System;
 namespace Hyper.PCAAprilTag
 {
     [Serializable]
-    public struct IdSizePair
+    public struct IdSizePair : IEquatable<IdSizePair>
     {
         public int id;
         public float size;
+
+        public IdSizePair(int id, float size)
+        {
+            this.id = id;
+            this.size = size;
+        }
+
+        public bool Equals(IdSizePair other)
+        {
+            return id == other.id;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is IdSizePair other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            return id;
+        }
     }
 }
diff --git a/Runtime/Scripts/KeijiroAprilTag/Utils.cs b/Runtime/Scripts/KeijiroAprilTag/Utils.cs
index 150920c28551d09a92761eb2844f4992b41667bc..69c7c60fe0d68e2c0f713dc539db1c6995b32d7d 100644
--- a/Runtime/Scripts/KeijiroAprilTag/Utils.cs
+++ b/Runtime/Scripts/KeijiroAprilTag/Utils.cs
@@ -1,4 +1,3 @@
-using AprilTag;
 using System.Collections.Generic;
 using Unity.Collections;
 using Unity.Mathematics;
diff --git a/Runtime/Scripts/Lifo.cs b/Runtime/Scripts/Lifo.cs
index 480ec6df4577b8b0122ab5a97ca2df92609afc86..8988e45c0af051438e75f76d8eb20e81806fc435 100644
--- a/Runtime/Scripts/Lifo.cs
+++ b/Runtime/Scripts/Lifo.cs
@@ -1,45 +1,52 @@
 using System.Collections.Generic;
 using System.Linq;
 
-// LIFO (Last-In-First-Out) List
-public class Lifo<T>
+namespace Hyper.PCAAprilTag
 {
-    private readonly int _capacity;
-    private readonly List<T> _list;
-
-    public Lifo(int capacity)
+    /// <summary>
+    /// LIFO (Last-In-First-Out) List
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    public class Lifo<T>
     {
-        _capacity = capacity;
-        _list = new List<T>(capacity);
-    }
+        private readonly int _capacity;
+        private readonly List<T> _list;
 
-    public void Push(T item)
-    {
-        if (_list.Count >= _capacity)
+        public Lifo(int capacity)
         {
-            _list.RemoveAt(0);
+            _capacity = capacity;
+            _list = new List<T>(capacity);
         }
-        _list.Add(item);
-    }
 
-    public T Pop()
-    {
-        if (_list.Count == 0) return default;
+        public void Push(T item)
+        {
+            if (_list.Count >= _capacity)
+            {
+                _list.RemoveAt(0);
+            }
 
-        var item = _list[0];
-        _list.RemoveAt(0);
-        return item;
-    }
+            _list.Add(item);
+        }
 
-    public T Top => _list.Last();
+        public T Pop()
+        {
+            if (_list.Count == 0) return default;
 
-    public T this[int index] => _list[index];
+            var item = _list[0];
+            _list.RemoveAt(0);
+            return item;
+        }
 
-    public bool IsEmpty => _list.Count == 0;
+        public T Top => _list.Last();
 
-    public bool IsFull => _list.Count >= _capacity;
+        public T this[int index] => _list[index];
 
-    public int Count => _list.Count;
+        public bool IsEmpty => _list.Count == 0;
 
-    public List<T> ToList() => new List<T>(_list);
+        public bool IsFull => _list.Count >= _capacity;
+
+        public int Count => _list.Count;
+
+        public List<T> ToList() => new List<T>(_list);
+    }
 }
diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs
deleted file mode 100644
index 67b1e2fc1e4c050ba74e8ed3fe4f4c878998034a..0000000000000000000000000000000000000000
--- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) Meta Platforms, Inc. and affiliates.
-
-using UnityEngine;
-
-public static class PassthroughCameraDebugger
-{
-    public enum DebuglevelEnum
-    {
-        ALL,
-        NONE,
-        ONLY_ERROR,
-        ONLY_LOG,
-        ONLY_WARNING
-    }
-
-    public static DebuglevelEnum DebugLevel = DebuglevelEnum.ALL;
-
-    /// <summary>
-    /// Send debug information to Unity console based on DebugType and DebugLevel
-    /// </summary>
-    /// <param name="mType"></param>
-    /// <param name="message"></param>
-    public static void DebugMessage(LogType mType, string message)
-    {
-        switch (mType)
-        {
-            case LogType.Error:
-                if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_ERROR)
-                {
-                    Debug.LogError(message);
-                }
-                break;
-            case LogType.Log:
-                if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_LOG)
-                {
-                    Debug.Log(message);
-                }
-                break;
-            case LogType.Warning:
-                if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_WARNING)
-                {
-                    Debug.LogWarning(message);
-                }
-                break;
-        }
-    }
-}
diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta
deleted file mode 100644
index 89ad076bd4b424d46834c221f4b7d144aa5c639c..0000000000000000000000000000000000000000
--- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 3a8ac5b7682fa4c2491fe8382784bad5
\ No newline at end of file
diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs
index fb166ba29c4d37f24e36eec121497a42a22affb1..9d816289d2b3686bdf37db1323aa12cf6eae4ad6 100644
--- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs
+++ b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs
@@ -5,84 +5,89 @@ using System.Linq;
 using UnityEngine;
 #if UNITY_ANDROID
 using UnityEngine.Android;
-using PCD = PassthroughCameraDebugger;
 #pragma warning disable CS0618 // Type or member is obsolete
 #endif
 
-/// <summary>
-/// PLEASE NOTE: Unity doesn't support requesting multiple permissions at the same time with <see cref="Permission.RequestUserPermissions"/> on Android.
-/// This component is a sample and shouldn't be used simultaneously with other scripts that manage Android permissions.
-/// </summary>
-public class PassthroughCameraPermissions : MonoBehaviour
+namespace Hyper.PCAAprilTag
 {
-    [SerializeField] public List<string> PermissionRequestsOnStartup = new() { OVRPermissionsRequester.ScenePermission };
-
-    public static readonly string[] CameraPermissions =
-    {
-        "android.permission.CAMERA",          // Required to use WebCamTexture object.
-        "horizonos.permission.HEADSET_CAMERA" // Required to access the Passthrough Camera API in Horizon OS v74 and above.
-    };
-
-    public static bool? HasCameraPermission { get; private set; }
-    private static bool s_askedOnce;
-
-#if UNITY_ANDROID
     /// <summary>
-    /// Request camera permission if the permission is not authorized by the user.
+    /// PLEASE NOTE: Unity doesn't support requesting multiple permissions at the same time with <see cref="Permission.RequestUserPermissions"/> on Android.
+    /// This component is a sample and shouldn't be used simultaneously with other scripts that manage Android permissions.
     /// </summary>
-    public void AskCameraPermissions()
+    public class PassthroughCameraPermissions : MonoBehaviour
     {
-        if (s_askedOnce)
-        {
-            return;
-        }
-        s_askedOnce = true;
-        if (IsAllCameraPermissionsGranted())
+        [SerializeField]
+        public List<string> PermissionRequestsOnStartup = new() { OVRPermissionsRequester.ScenePermission };
+
+        public static readonly string[] CameraPermissions =
         {
-            HasCameraPermission = true;
-            PCD.DebugMessage(LogType.Log, "PCA: All camera permissions granted.");
-        }
-        else
+            "android.permission.CAMERA", // Required to use WebCamTexture object.
+            "horizonos.permission.HEADSET_CAMERA" // Required to access the Passthrough Camera API in Horizon OS v74 and above.
+        };
+
+        public static bool? HasCameraPermission { get; private set; }
+        private static bool s_askedOnce;
+
+#if UNITY_ANDROID
+        /// <summary>
+        /// Request camera permission if the permission is not authorized by the user.
+        /// </summary>
+        public void AskCameraPermissions()
         {
-            PCD.DebugMessage(LogType.Log, "PCA: Requesting camera permissions.");
+            if (s_askedOnce)
+            {
+                return;
+            }
 
-            var callbacks = new PermissionCallbacks();
-            callbacks.PermissionDenied += PermissionCallbacksPermissionDenied;
-            callbacks.PermissionGranted += PermissionCallbacksPermissionGranted;
-            callbacks.PermissionDeniedAndDontAskAgain += PermissionCallbacksPermissionDenied;
+            s_askedOnce = true;
+            if (IsAllCameraPermissionsGranted())
+            {
+                HasCameraPermission = true;
+                DebugLog.Log(LogType.Log, "PCA: All camera permissions granted.");
+            }
+            else
+            {
+                DebugLog.Log(LogType.Log, "PCA: Requesting camera permissions.");
 
-            // It's important to request all necessary permissions in one request because only one 'PermissionCallbacks' instance is supported at a time.
-            var allPermissions = CameraPermissions.Concat(PermissionRequestsOnStartup).ToArray();
-            Permission.RequestUserPermissions(allPermissions, callbacks);
-        }
-    }
+                var callbacks = new PermissionCallbacks();
+                callbacks.PermissionDenied += PermissionCallbacksPermissionDenied;
+                callbacks.PermissionGranted += PermissionCallbacksPermissionGranted;
+                callbacks.PermissionDeniedAndDontAskAgain += PermissionCallbacksPermissionDenied;
 
-    /// <summary>
-    /// Permission Granted callback
-    /// </summary>
-    /// <param name="permissionName"></param>
-    private static void PermissionCallbacksPermissionGranted(string permissionName)
-    {
-        PCD.DebugMessage(LogType.Log, $"PCA: Permission {permissionName} Granted");
+                // It's important to request all necessary permissions in one request because only one 'PermissionCallbacks' instance is supported at a time.
+                var allPermissions = CameraPermissions.Concat(PermissionRequestsOnStartup).ToArray();
+                Permission.RequestUserPermissions(allPermissions, callbacks);
+            }
+        }
 
-        // Only initialize the WebCamTexture object if both permissions are granted
-        if (IsAllCameraPermissionsGranted())
+        /// <summary>
+        /// Permission Granted callback
+        /// </summary>
+        /// <param name="permissionName"></param>
+        private static void PermissionCallbacksPermissionGranted(string permissionName)
         {
-            HasCameraPermission = true;
+            DebugLog.Log(LogType.Log, $"PCA: Permission {permissionName} Granted");
+
+            // Only initialize the WebCamTexture object if both permissions are granted
+            if (IsAllCameraPermissionsGranted())
+            {
+                HasCameraPermission = true;
+            }
         }
-    }
 
-    /// <summary>
-    /// Permission Denied callback.
-    /// </summary>
-    /// <param name="permissionName"></param>
-    private static void PermissionCallbacksPermissionDenied(string permissionName)
-    {
-        PCD.DebugMessage(LogType.Warning, $"PCA: Permission {permissionName} Denied");
-        HasCameraPermission = false;
-        s_askedOnce = false;
-    }
+        /// <summary>
+        /// Permission Denied callback.
+        /// </summary>
+        /// <param name="permissionName"></param>
+        private static void PermissionCallbacksPermissionDenied(string permissionName)
+        {
+            DebugLog.Log(LogType.Warning, $"PCA: Permission {permissionName} Denied");
+            HasCameraPermission = false;
+            s_askedOnce = false;
+        }
 
-    private static bool IsAllCameraPermissionsGranted() => CameraPermissions.All(Permission.HasUserAuthorizedPermission);
+        private static bool IsAllCameraPermissionsGranted() =>
+            CameraPermissions.All(Permission.HasUserAuthorizedPermission);
 #endif
+    }
 }
diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs
index b5ae67f1c3168dd37cf23f886fecc3131a055a3e..93cc547cbc3371bea62a9fb01017aa2d627ab739 100644
--- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs
+++ b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs
@@ -6,362 +6,378 @@ using System.Collections.Generic;
 using UnityEngine;
 using UnityEngine.Assertions;
 
-public static class PassthroughCameraUtils
+namespace Hyper.PCAAprilTag
 {
-    // The Horizon OS starts supporting PCA with v74.
-    public const int MINSUPPORTOSVERSION = 74;
+    public static class PassthroughCameraUtils
+    {
+        // The Horizon OS starts supporting PCA with v74.
+        public const int MINSUPPORTOSVERSION = 74;
 
-    // The only pixel format supported atm
-    private const int YUV_420_888 = 0x00000023;
+        // The only pixel format supported atm
+        private const int YUV_420_888 = 0x00000023;
 
-    private static AndroidJavaObject s_currentActivity;
-    private static AndroidJavaObject s_cameraManager;
-    private static bool? s_isSupported;
-    private static int? s_horizonOsVersion;
+        private static AndroidJavaObject s_currentActivity;
+        private static AndroidJavaObject s_cameraManager;
+        private static bool? s_isSupported;
+        private static int? s_horizonOsVersion;
 
-    // Caches
-    internal static readonly Dictionary<PassthroughCameraEye, (string id, int index)> CameraEyeToCameraIdMap = new();
-    private static readonly ConcurrentDictionary<PassthroughCameraEye, List<Vector2Int>> s_cameraOutputSizes = new();
-    private static readonly ConcurrentDictionary<string, AndroidJavaObject> s_cameraCharacteristicsMap = new();
-    private static readonly OVRPose?[] s_cachedCameraPosesRelativeToHead = new OVRPose?[2];
+        // Caches
+        internal static readonly Dictionary<PassthroughCameraEye, (string id, int index)>
+            CameraEyeToCameraIdMap = new();
 
-    /// <summary>
-    /// Get the Horizon OS version number on the headset
-    /// </summary>
-    public static int? HorizonOSVersion
-    {
-        get
+        private static readonly ConcurrentDictionary<PassthroughCameraEye, List<Vector2Int>>
+            s_cameraOutputSizes = new();
+
+        private static readonly ConcurrentDictionary<string, AndroidJavaObject> s_cameraCharacteristicsMap = new();
+        private static readonly OVRPose?[] s_cachedCameraPosesRelativeToHead = new OVRPose?[2];
+
+        /// <summary>
+        /// Get the Horizon OS version number on the headset
+        /// </summary>
+        public static int? HorizonOSVersion
         {
-            if (!s_horizonOsVersion.HasValue)
+            get
             {
-                var vrosClass = new AndroidJavaClass("vros.os.VrosBuild");
-                s_horizonOsVersion = vrosClass.CallStatic<int>("getSdkVersion");
+                if (!s_horizonOsVersion.HasValue)
+                {
+                    var vrosClass = new AndroidJavaClass("vros.os.VrosBuild");
+                    s_horizonOsVersion = vrosClass.CallStatic<int>("getSdkVersion");
 #if OVR_INTERNAL_CODE
                 // 10000 means that the build doesn't have a proper release version, and it is still in Mainline,
                 // not in a release branch.
 #endif // OVR_INTERNAL_CODE
-                if (s_horizonOsVersion == 10000)
-                {
-                    s_horizonOsVersion = -1;
+                    if (s_horizonOsVersion == 10000)
+                    {
+                        s_horizonOsVersion = -1;
+                    }
                 }
-            }
 
-            return s_horizonOsVersion.Value != -1 ? s_horizonOsVersion.Value : null;
+                return s_horizonOsVersion.Value != -1 ? s_horizonOsVersion.Value : null;
+            }
         }
-    }
 
-    /// <summary>
-    /// Returns true if the current headset supports Passthrough Camera API
-    /// </summary>
-    public static bool IsSupported
-    {
-        get
+        /// <summary>
+        /// Returns true if the current headset supports Passthrough Camera API
+        /// </summary>
+        public static bool IsSupported
         {
-            if (!s_isSupported.HasValue)
+            get
             {
-                var headset = OVRPlugin.GetSystemHeadsetType();
-                return (headset == OVRPlugin.SystemHeadset.Meta_Quest_3 ||
-                        headset == OVRPlugin.SystemHeadset.Meta_Quest_3S) &&
-                       (!HorizonOSVersion.HasValue || HorizonOSVersion >= MINSUPPORTOSVERSION);
-            }
+                if (!s_isSupported.HasValue)
+                {
+                    var headset = OVRPlugin.GetSystemHeadsetType();
+                    return (headset == OVRPlugin.SystemHeadset.Meta_Quest_3 ||
+                            headset == OVRPlugin.SystemHeadset.Meta_Quest_3S) &&
+                           (!HorizonOSVersion.HasValue || HorizonOSVersion >= MINSUPPORTOSVERSION);
+                }
 
-            return s_isSupported.Value;
+                return s_isSupported.Value;
+            }
         }
-    }
-
-    /// <summary>
-    /// Provides a list of resolutions supported by the passthrough camera. Developers should use one of those
-    /// when initializing the camera.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    public static List<Vector2Int> GetOutputSizes(PassthroughCameraEye cameraEye)
-    {
-        return s_cameraOutputSizes.GetOrAdd(cameraEye, GetOutputSizesInternal(cameraEye));
-    }
-
-    /// <summary>
-    /// Returns the camera intrinsics for a specified passthrough camera. All the intrinsics values are provided
-    /// in pixels. The resolution value is the maximum resolution available for the camera.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    public static PassthroughCameraIntrinsics GetCameraIntrinsics(PassthroughCameraEye cameraEye)
-    {
-        var cameraCharacteristics = GetCameraCharacteristics(cameraEye);
-        var intrinsicsArr = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_INTRINSIC_CALIBRATION");
-
-        // Querying the camera resolution for which the intrinsics are provided
-        // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
-        // This is a Rect of 4 elements: [bottom, left, right, top] with (0,0) at top-left corner.
-        using var sensorSize = GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE");
 
-        return new PassthroughCameraIntrinsics
+        /// <summary>
+        /// Provides a list of resolutions supported by the passthrough camera. Developers should use one of those
+        /// when initializing the camera.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        public static List<Vector2Int> GetOutputSizes(PassthroughCameraEye cameraEye)
         {
-            FocalLength = new Vector2(intrinsicsArr[0], intrinsicsArr[1]),
-            PrincipalPoint = new Vector2(intrinsicsArr[2], intrinsicsArr[3]),
-            Resolution = new Vector2Int(sensorSize.Get<int>("right"), sensorSize.Get<int>("bottom")),
-            Skew = intrinsicsArr[4]
-        };
-    }
+            return s_cameraOutputSizes.GetOrAdd(cameraEye, GetOutputSizesInternal(cameraEye));
+        }
 
-    /// <summary>
-    /// Returns an Android Camera2 API's cameraId associated with the passthrough camera specified in the argument.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    /// <exception cref="ApplicationException">Throws an exception if the code was not able to find cameraId</exception>
-    public static string GetCameraIdByEye(PassthroughCameraEye cameraEye)
-    {
-        _ = EnsureInitialized();
+        /// <summary>
+        /// Returns the camera intrinsics for a specified passthrough camera. All the intrinsics values are provided
+        /// in pixels. The resolution value is the maximum resolution available for the camera.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        public static PassthroughCameraIntrinsics GetCameraIntrinsics(PassthroughCameraEye cameraEye)
+        {
+            var cameraCharacteristics = GetCameraCharacteristics(cameraEye);
+            var intrinsicsArr = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_INTRINSIC_CALIBRATION");
 
-        return !CameraEyeToCameraIdMap.TryGetValue(cameraEye, out var value)
-            ? throw new ApplicationException($"Cannot find cameraId for the eye {cameraEye}")
-            : value.id;
-    }
+            // Querying the camera resolution for which the intrinsics are provided
+            // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
+            // This is a Rect of 4 elements: [bottom, left, right, top] with (0,0) at top-left corner.
+            using var sensorSize = GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics,
+                "SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE");
 
-    /// <summary>
-    /// Returns the world pose of a passthrough camera at a given time.
-    /// The LENS_POSE_TRANSLATION and LENS_POSE_ROTATION keys in 'android.hardware.camera2' are relative to the origin, so they can be cached to improve performance.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    /// <returns>The passthrough camera's world pose</returns>
-    public static Pose GetCameraPoseInWorld(PassthroughCameraEye cameraEye)
-    {
-        var index = cameraEye == PassthroughCameraEye.Left ? 0 : 1;
+            return new PassthroughCameraIntrinsics
+            {
+                FocalLength = new Vector2(intrinsicsArr[0], intrinsicsArr[1]),
+                PrincipalPoint = new Vector2(intrinsicsArr[2], intrinsicsArr[3]),
+                Resolution = new Vector2Int(sensorSize.Get<int>("right"), sensorSize.Get<int>("bottom")),
+                Skew = intrinsicsArr[4]
+            };
+        }
 
-        if (s_cachedCameraPosesRelativeToHead[index] == null)
+        /// <summary>
+        /// Returns an Android Camera2 API's cameraId associated with the passthrough camera specified in the argument.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        /// <exception cref="ApplicationException">Throws an exception if the code was not able to find cameraId</exception>
+        public static string GetCameraIdByEye(PassthroughCameraEye cameraEye)
         {
-            var cameraId = GetCameraIdByEye(cameraEye);
-            using var cameraCharacteristics = s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId);
+            _ = EnsureInitialized();
 
-            var cameraTranslation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_TRANSLATION");
-            var p_headFromCamera = new Vector3(cameraTranslation[0], cameraTranslation[1], -cameraTranslation[2]);
-
-            var cameraRotation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_ROTATION");
-            var q_cameraFromHead = new Quaternion(-cameraRotation[0], -cameraRotation[1], cameraRotation[2], cameraRotation[3]);
+            return !CameraEyeToCameraIdMap.TryGetValue(cameraEye, out var value)
+                ? throw new ApplicationException($"Cannot find cameraId for the eye {cameraEye}")
+                : value.id;
+        }
 
-            var q_headFromCamera = Quaternion.Inverse(q_cameraFromHead);
+        /// <summary>
+        /// Returns the world pose of a passthrough camera at a given time.
+        /// The LENS_POSE_TRANSLATION and LENS_POSE_ROTATION keys in 'android.hardware.camera2' are relative to the origin, so they can be cached to improve performance.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        /// <returns>The passthrough camera's world pose</returns>
+        public static Pose GetCameraPoseInWorld(PassthroughCameraEye cameraEye)
+        {
+            var index = cameraEye == PassthroughCameraEye.Left ? 0 : 1;
 
-            s_cachedCameraPosesRelativeToHead[index] = new OVRPose
+            if (s_cachedCameraPosesRelativeToHead[index] == null)
             {
-                position = p_headFromCamera,
-                orientation = q_headFromCamera
-            };
-        }
+                var cameraId = GetCameraIdByEye(cameraEye);
+                using var cameraCharacteristics =
+                    s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId);
 
-        var headFromCamera = s_cachedCameraPosesRelativeToHead[index].Value;
-        var worldFromHead = OVRPlugin.GetNodePoseStateImmediate(OVRPlugin.Node.Head).Pose.ToOVRPose();
-        var worldFromCamera = worldFromHead * headFromCamera;
-        worldFromCamera.orientation *= Quaternion.Euler(180, 0, 0);
+                var cameraTranslation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_TRANSLATION");
+                var p_headFromCamera = new Vector3(cameraTranslation[0], cameraTranslation[1], -cameraTranslation[2]);
 
-        return new Pose(worldFromCamera.position, worldFromCamera.orientation);
-    }
+                var cameraRotation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_ROTATION");
+                var q_cameraFromHead = new Quaternion(-cameraRotation[0], -cameraRotation[1], cameraRotation[2],
+                    cameraRotation[3]);
 
-    /// <summary>
-    /// Returns a 3D ray in the world space which starts from the passthrough camera origin and passes through the
-    /// 2D camera pixel.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the
-    ///     maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/>
-    ///     or <see cref="GetOutputSizes"/> methods.
-    /// </param>
-    public static Ray ScreenPointToRayInWorld(PassthroughCameraEye cameraEye, Vector2Int screenPoint)
-    {
-        var rayInCamera = ScreenPointToRayInCamera(cameraEye, screenPoint);
-        var cameraPoseInWorld = GetCameraPoseInWorld(cameraEye);
-        var rayDirectionInWorld = cameraPoseInWorld.rotation * rayInCamera.direction;
-        return new Ray(cameraPoseInWorld.position, rayDirectionInWorld);
-    }
+                var q_headFromCamera = Quaternion.Inverse(q_cameraFromHead);
 
-    /// <summary>
-    /// Returns a 3D ray in the camera space which starts from the passthrough camera origin - which is always
-    /// (0, 0, 0) - and passes through the 2D camera pixel.
-    /// </summary>
-    /// <param name="cameraEye">The passthrough camera</param>
-    /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the
-    /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/>
-    /// or <see cref="GetOutputSizes"/> methods.
-    /// </param>
-    public static Ray ScreenPointToRayInCamera(PassthroughCameraEye cameraEye, Vector2Int screenPoint)
-    {
-        var intrinsics = GetCameraIntrinsics(cameraEye);
-        var directionInCamera = new Vector3
-        {
-            x = (screenPoint.x - intrinsics.PrincipalPoint.x) / intrinsics.FocalLength.x,
-            y = (screenPoint.y - intrinsics.PrincipalPoint.y) / intrinsics.FocalLength.y,
-            z = 1
-        };
+                s_cachedCameraPosesRelativeToHead[index] = new OVRPose
+                {
+                    position = p_headFromCamera,
+                    orientation = q_headFromCamera
+                };
+            }
 
-        return new Ray(Vector3.zero, directionInCamera);
-    }
+            var headFromCamera = s_cachedCameraPosesRelativeToHead[index].Value;
+            var worldFromHead = OVRPlugin.GetNodePoseStateImmediate(OVRPlugin.Node.Head).Pose.ToOVRPose();
+            var worldFromCamera = worldFromHead * headFromCamera;
+            worldFromCamera.orientation *= Quaternion.Euler(180, 0, 0);
 
-    #region Private methods
+            return new Pose(worldFromCamera.position, worldFromCamera.orientation);
+        }
 
-    internal static bool EnsureInitialized()
-    {
-        if (CameraEyeToCameraIdMap.Count == 2)
+        /// <summary>
+        /// Returns a 3D ray in the world space which starts from the passthrough camera origin and passes through the
+        /// 2D camera pixel.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the
+        ///     maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/>
+        ///     or <see cref="GetOutputSizes"/> methods.
+        /// </param>
+        public static Ray ScreenPointToRayInWorld(PassthroughCameraEye cameraEye, Vector2Int screenPoint)
         {
-            return true;
+            var rayInCamera = ScreenPointToRayInCamera(cameraEye, screenPoint);
+            var cameraPoseInWorld = GetCameraPoseInWorld(cameraEye);
+            var rayDirectionInWorld = cameraPoseInWorld.rotation * rayInCamera.direction;
+            return new Ray(cameraPoseInWorld.position, rayDirectionInWorld);
         }
 
-        Debug.Log($"PCA: PassthroughCamera - Initializing...");
-        using var activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
-        s_currentActivity = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
-        s_cameraManager = s_currentActivity.Call<AndroidJavaObject>("getSystemService", "camera");
-        Assert.IsNotNull(s_cameraManager, "Camera manager has not been provided by the Android system");
+        /// <summary>
+        /// Returns a 3D ray in the camera space which starts from the passthrough camera origin - which is always
+        /// (0, 0, 0) - and passes through the 2D camera pixel.
+        /// </summary>
+        /// <param name="cameraEye">The passthrough camera</param>
+        /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the
+        /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/>
+        /// or <see cref="GetOutputSizes"/> methods.
+        /// </param>
+        public static Ray ScreenPointToRayInCamera(PassthroughCameraEye cameraEye, Vector2Int screenPoint)
+        {
+            var intrinsics = GetCameraIntrinsics(cameraEye);
+            var directionInCamera = new Vector3
+            {
+                x = (screenPoint.x - intrinsics.PrincipalPoint.x) / intrinsics.FocalLength.x,
+                y = (screenPoint.y - intrinsics.PrincipalPoint.y) / intrinsics.FocalLength.y,
+                z = 1
+            };
+
+            return new Ray(Vector3.zero, directionInCamera);
+        }
 
-        var cameraIds = GetCameraIdList();
-        Debug.Log($"PCA: PassthroughCamera - cameraId list is {string.Join(", ", cameraIds)}");
+        #region Private methods
 
-        for (var idIndex = 0; idIndex < cameraIds.Length; idIndex++)
+        internal static bool EnsureInitialized()
         {
-            var cameraId = cameraIds[idIndex];
-            CameraSource? cameraSource = null;
-            CameraPosition? cameraPosition = null;
-
-            var cameraCharacteristics = GetCameraCharacteristics(cameraId);
-            using var keysList = cameraCharacteristics.Call<AndroidJavaObject>("getKeys");
-            var size = keysList.Call<int>("size");
-            for (var i = 0; i < size; i++)
+            if (CameraEyeToCameraIdMap.Count == 2)
             {
-                using var key = keysList.Call<AndroidJavaObject>("get", i);
-                var keyName = key.Call<string>("getName");
+                return true;
+            }
 
-                if (string.Equals(keyName, "com.meta.extra_metadata.camera_source", StringComparison.OrdinalIgnoreCase))
+            DebugLog.Log(LogType.Log, $"PCA: PassthroughCamera - Initializing...");
+            using var activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+            s_currentActivity = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
+            s_cameraManager = s_currentActivity.Call<AndroidJavaObject>("getSystemService", "camera");
+            Assert.IsNotNull(s_cameraManager, "Camera manager has not been provided by the Android system");
+
+            var cameraIds = GetCameraIdList();
+            DebugLog.Log(LogType.Log, $"PCA: PassthroughCamera - cameraId list is {string.Join(", ", cameraIds)}");
+
+            for (var idIndex = 0; idIndex < cameraIds.Length; idIndex++)
+            {
+                var cameraId = cameraIds[idIndex];
+                CameraSource? cameraSource = null;
+                CameraPosition? cameraPosition = null;
+
+                var cameraCharacteristics = GetCameraCharacteristics(cameraId);
+                using var keysList = cameraCharacteristics.Call<AndroidJavaObject>("getKeys");
+                var size = keysList.Call<int>("size");
+                for (var i = 0; i < size; i++)
                 {
-                    // Both `com.meta.extra_metadata.camera_source` and `com.meta.extra_metadata.camera_source` are
-                    // custom camera fields which are stored as arrays of size 1, instead of single values.
-                    // We have to read those values correspondingly
-                    var cameraSourceArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key);
-                    if (cameraSourceArr == null || cameraSourceArr.Length != 1)
-                        continue;
-
-                    cameraSource = (CameraSource)cameraSourceArr[0];
+                    using var key = keysList.Call<AndroidJavaObject>("get", i);
+                    var keyName = key.Call<string>("getName");
+
+                    if (string.Equals(keyName, "com.meta.extra_metadata.camera_source",
+                            StringComparison.OrdinalIgnoreCase))
+                    {
+                        // Both `com.meta.extra_metadata.camera_source` and `com.meta.extra_metadata.camera_source` are
+                        // custom camera fields which are stored as arrays of size 1, instead of single values.
+                        // We have to read those values correspondingly
+                        var cameraSourceArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key);
+                        if (cameraSourceArr == null || cameraSourceArr.Length != 1)
+                            continue;
+
+                        cameraSource = (CameraSource)cameraSourceArr[0];
+                    }
+                    else if (string.Equals(keyName, "com.meta.extra_metadata.position",
+                                 StringComparison.OrdinalIgnoreCase))
+                    {
+                        var cameraPositionArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key);
+                        if (cameraPositionArr == null || cameraPositionArr.Length != 1)
+                            continue;
+
+                        cameraPosition = (CameraPosition)cameraPositionArr[0];
+                    }
                 }
-                else if (string.Equals(keyName, "com.meta.extra_metadata.position", StringComparison.OrdinalIgnoreCase))
-                {
-                    var cameraPositionArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key);
-                    if (cameraPositionArr == null || cameraPositionArr.Length != 1)
-                        continue;
 
-                    cameraPosition = (CameraPosition)cameraPositionArr[0];
+                if (!cameraSource.HasValue || !cameraPosition.HasValue ||
+                    cameraSource.Value != CameraSource.Passthrough)
+                    continue;
+
+                switch (cameraPosition)
+                {
+                    case CameraPosition.Left:
+                        DebugLog.Log(LogType.Log, $"PCA: Found left passthrough cameraId = {cameraId}");
+                        CameraEyeToCameraIdMap[PassthroughCameraEye.Left] = (cameraId, idIndex);
+                        break;
+                    case CameraPosition.Right:
+                        DebugLog.Log(LogType.Log, $"PCA: Found right passthrough cameraId = {cameraId}");
+                        CameraEyeToCameraIdMap[PassthroughCameraEye.Right] = (cameraId, idIndex);
+                        break;
+                    default:
+                        throw new ApplicationException($"Cannot parse Camera Position value {cameraPosition}");
                 }
             }
 
-            if (!cameraSource.HasValue || !cameraPosition.HasValue || cameraSource.Value != CameraSource.Passthrough)
-                continue;
+            return CameraEyeToCameraIdMap.Count == 2;
+        }
 
-            switch (cameraPosition)
-            {
-                case CameraPosition.Left:
-                    Debug.Log($"PCA: Found left passthrough cameraId = {cameraId}");
-                    CameraEyeToCameraIdMap[PassthroughCameraEye.Left] = (cameraId, idIndex);
-                    break;
-                case CameraPosition.Right:
-                    Debug.Log($"PCA: Found right passthrough cameraId = {cameraId}");
-                    CameraEyeToCameraIdMap[PassthroughCameraEye.Right] = (cameraId, idIndex);
-                    break;
-                default:
-                    throw new ApplicationException($"Cannot parse Camera Position value {cameraPosition}");
-            }
+        internal static bool IsPassthroughEnabled()
+        {
+            return OVRManager.IsInsightPassthroughSupported() &&
+                   OVRManager.IsInsightPassthroughInitialized() &&
+                   OVRManager.instance.isInsightPassthroughEnabled;
         }
 
-        return CameraEyeToCameraIdMap.Count == 2;
-    }
+        private static string[] GetCameraIdList()
+        {
+            return s_cameraManager.Call<string[]>("getCameraIdList");
+        }
 
-    internal static bool IsPassthroughEnabled()
-    {
-        return OVRManager.IsInsightPassthroughSupported() &&
-            OVRManager.IsInsightPassthroughInitialized() &&
-            OVRManager.instance.isInsightPassthroughEnabled;
-    }
+        private static List<Vector2Int> GetOutputSizesInternal(PassthroughCameraEye cameraEye)
+        {
+            _ = EnsureInitialized();
 
-    private static string[] GetCameraIdList()
-    {
-        return s_cameraManager.Call<string[]>("getCameraIdList");
-    }
+            var cameraId = GetCameraIdByEye(cameraEye);
+            var cameraCharacteristics = GetCameraCharacteristics(cameraId);
+            using var configurationMap =
+                GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SCALER_STREAM_CONFIGURATION_MAP");
+            var outputSizes = configurationMap.Call<AndroidJavaObject[]>("getOutputSizes", YUV_420_888);
 
-    private static List<Vector2Int> GetOutputSizesInternal(PassthroughCameraEye cameraEye)
-    {
-        _ = EnsureInitialized();
+            var result = new List<Vector2Int>();
+            foreach (var outputSize in outputSizes)
+            {
+                var width = outputSize.Call<int>("getWidth");
+                var height = outputSize.Call<int>("getHeight");
+                result.Add(new Vector2Int(width, height));
+            }
 
-        var cameraId = GetCameraIdByEye(cameraEye);
-        var cameraCharacteristics = GetCameraCharacteristics(cameraId);
-        using var configurationMap =
-            GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SCALER_STREAM_CONFIGURATION_MAP");
-        var outputSizes = configurationMap.Call<AndroidJavaObject[]>("getOutputSizes", YUV_420_888);
+            foreach (var obj in outputSizes)
+            {
+                obj?.Dispose();
+            }
 
-        var result = new List<Vector2Int>();
-        foreach (var outputSize in outputSizes)
-        {
-            var width = outputSize.Call<int>("getWidth");
-            var height = outputSize.Call<int>("getHeight");
-            result.Add(new Vector2Int(width, height));
+            return result;
         }
 
-        foreach (var obj in outputSizes)
+        private static AndroidJavaObject GetCameraCharacteristics(string cameraId)
         {
-            obj?.Dispose();
+            return s_cameraCharacteristicsMap.GetOrAdd(cameraId,
+                _ => s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId));
         }
 
-        return result;
-    }
-
-    private static AndroidJavaObject GetCameraCharacteristics(string cameraId)
-    {
-        return s_cameraCharacteristicsMap.GetOrAdd(cameraId,
-            _ => s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId));
-    }
+        private static AndroidJavaObject GetCameraCharacteristics(PassthroughCameraEye eye)
+        {
+            var cameraId = GetCameraIdByEye(eye);
+            return GetCameraCharacteristics(cameraId);
+        }
 
-    private static AndroidJavaObject GetCameraCharacteristics(PassthroughCameraEye eye)
-    {
-        var cameraId = GetCameraIdByEye(eye);
-        return GetCameraCharacteristics(cameraId);
-    }
+        private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, string keyStr)
+        {
+            using var key = cameraCharacteristics.GetStatic<AndroidJavaObject>(keyStr);
+            return GetCameraValueByKey<T>(cameraCharacteristics, key);
+        }
 
-    private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, string keyStr)
-    {
-        using var key = cameraCharacteristics.GetStatic<AndroidJavaObject>(keyStr);
-        return GetCameraValueByKey<T>(cameraCharacteristics, key);
-    }
+        private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, AndroidJavaObject key)
+        {
+            return cameraCharacteristics.Call<T>("get", key);
+        }
 
-    private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, AndroidJavaObject key)
-    {
-        return cameraCharacteristics.Call<T>("get", key);
-    }
+        private enum CameraSource
+        {
+            Passthrough = 0
+        }
 
-    private enum CameraSource
-    {
-        Passthrough = 0
-    }
+        private enum CameraPosition
+        {
+            Left = 0,
+            Right = 1
+        }
 
-    private enum CameraPosition
-    {
-        Left = 0,
-        Right = 1
+        #endregion Private methods
     }
 
-    #endregion Private methods
-}
-
-/// <summary>
-/// Contains camera intrinsics, which describe physical characteristics of a passthrough camera
-/// </summary>
-public struct PassthroughCameraIntrinsics
-{
     /// <summary>
-    /// The focal length in pixels
+    /// Contains camera intrinsics, which describe physical characteristics of a passthrough camera
     /// </summary>
-    public Vector2 FocalLength;
-    /// <summary>
-    /// The principal point from the top-left corner of the image, expressed in pixels
-    /// </summary>
-    public Vector2 PrincipalPoint;
-    /// <summary>
-    /// The resolution in pixels for which the intrinsics are defined
-    /// </summary>
-    public Vector2Int Resolution;
-    /// <summary>
-    /// The skew coefficient which represents the non-perpendicularity of the image sensor's x and y axes
-    /// </summary>
-    public float Skew;
+    public struct PassthroughCameraIntrinsics
+    {
+        /// <summary>
+        /// The focal length in pixels
+        /// </summary>
+        public Vector2 FocalLength;
+
+        /// <summary>
+        /// The principal point from the top-left corner of the image, expressed in pixels
+        /// </summary>
+        public Vector2 PrincipalPoint;
+
+        /// <summary>
+        /// The resolution in pixels for which the intrinsics are defined
+        /// </summary>
+        public Vector2Int Resolution;
+
+        /// <summary>
+        /// The skew coefficient which represents the non-perpendicularity of the image sensor's x and y axes
+        /// </summary>
+        public float Skew;
+    }
 }
diff --git a/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs b/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs
index 6c2b1a89182b31c9f9c246c2f0c2e5634abc3634..40dfd1442fc9572dc7112d79999624d92966a17d 100644
--- a/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs
+++ b/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs
@@ -4,143 +4,157 @@ using System.Collections;
 using System.Linq;
 using UnityEngine;
 using UnityEngine.Assertions;
-using PCD = PassthroughCameraDebugger;
 
-public class WebCamTextureManager : MonoBehaviour
+namespace Hyper.PCAAprilTag
 {
-    [SerializeField] public PassthroughCameraEye Eye = PassthroughCameraEye.Left;
-    [SerializeField, Tooltip("The requested resolution of the camera may not be supported by the chosen camera. In such cases, the closest available values will be used.\n\n" +
-                             "When set to (0,0), the highest supported resolution will be used.")]
-    public Vector2Int RequestedResolution;
-    [SerializeField] public PassthroughCameraPermissions CameraPermissions;
+    public class WebCamTextureManager : MonoBehaviour
+    {
+        [SerializeField] public PassthroughCameraEye Eye = PassthroughCameraEye.Left;
 
-    /// <summary>
-    /// Returns <see cref="WebCamTexture"/> reference if required permissions were granted and this component is enabled. Else, returns null.
-    /// </summary>
-    public WebCamTexture WebCamTexture { get; private set; }
+        [SerializeField, Tooltip(
+             "The requested resolution of the camera may not be supported by the chosen camera. In such cases, the closest available values will be used.\n\n" +
+             "When set to (0,0), the highest supported resolution will be used.")]
+        public Vector2Int RequestedResolution;
 
-    public bool IsReady { get; private set; }
+        [SerializeField] public PassthroughCameraPermissions CameraPermissions;
 
-    private bool m_hasPermission;
+        /// <summary>
+        /// Returns <see cref="WebCamTexture"/> reference if required permissions were granted and this component is enabled. Else, returns null.
+        /// </summary>
+        public WebCamTexture WebCamTexture { get; private set; }
 
-    private void Awake()
-    {
-        PCD.DebugMessage(LogType.Log, $"{nameof(WebCamTextureManager)}.{nameof(Awake)}() was called");
-        Assert.AreEqual(1, FindObjectsByType<WebCamTextureManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length,
-            $"PCA: Passthrough Camera: more than one {nameof(WebCamTextureManager)} component. Only one instance is allowed at a time. Current instance: {name}");
-#if UNITY_ANDROID
-        CameraPermissions.AskCameraPermissions();
-#endif
-    }
+        public bool IsReady { get; private set; }
 
-    private void OnEnable()
-    {
-        PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnEnable)}() was called");
-        if (!PassthroughCameraUtils.IsSupported)
+        private bool m_hasPermission;
+
+        private void Awake()
         {
-            PCD.DebugMessage(LogType.Log, "PCA: Passthrough Camera functionality is not supported by the current device." +
-                      $" Disabling {nameof(WebCamTextureManager)} object");
-            enabled = false;
-            return;
+            DebugLog.Log(LogType.Log, $"{nameof(WebCamTextureManager)}.{nameof(Awake)}() was called");
+            Assert.AreEqual(1,
+                FindObjectsByType<WebCamTextureManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length,
+                $"PCA: Passthrough Camera: more than one {nameof(WebCamTextureManager)} component. Only one instance is allowed at a time. Current instance: {name}");
+#if UNITY_ANDROID
+            CameraPermissions.AskCameraPermissions();
+#endif
         }
 
-        m_hasPermission = PassthroughCameraPermissions.HasCameraPermission == true;
-        if (!m_hasPermission)
+        private void OnEnable()
         {
-            PCD.DebugMessage(LogType.Error,
-                $"PCA: Passthrough Camera requires permission(s) {string.Join(" and ", PassthroughCameraPermissions.CameraPermissions)}. Waiting for them to be granted...");
-            return;
-        }
+            DebugLog.Log(LogType.Log, $"PCA: {nameof(OnEnable)}() was called");
+            if (!PassthroughCameraUtils.IsSupported)
+            {
+                DebugLog.Log(LogType.Log,
+                    "PCA: Passthrough Camera functionality is not supported by the current device." +
+                    $" Disabling {nameof(WebCamTextureManager)} object");
+                enabled = false;
+                return;
+            }
 
-        PCD.DebugMessage(LogType.Log, "PCA: All permissions have been granted");
-        _ = StartCoroutine(InitializeWebCamTexture());
-    }
+            m_hasPermission = PassthroughCameraPermissions.HasCameraPermission == true;
+            if (!m_hasPermission)
+            {
+                DebugLog.Log(LogType.Error,
+                    $"PCA: Passthrough Camera requires permission(s) {string.Join(" and ", PassthroughCameraPermissions.CameraPermissions)}. Waiting for them to be granted...");
+                return;
+            }
 
-    private void OnDisable()
-    {
-        PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnDisable)}() was called");
-        StopCoroutine(InitializeWebCamTexture());
-        if (WebCamTexture != null)
+            DebugLog.Log(LogType.Log, "PCA: All permissions have been granted");
+            _ = StartCoroutine(InitializeWebCamTexture());
+        }
+
+        private void OnDisable()
         {
-            WebCamTexture.Stop();
-            Destroy(WebCamTexture);
-            WebCamTexture = null;
+            DebugLog.Log(LogType.Log, $"PCA: {nameof(OnDisable)}() was called");
+            StopCoroutine(InitializeWebCamTexture());
+            if (WebCamTexture != null)
+            {
+                WebCamTexture.Stop();
+                Destroy(WebCamTexture);
+                WebCamTexture = null;
+            }
         }
-    }
 
-    private void Update()
-    {
-        if (!m_hasPermission)
+        private void Update()
         {
-            if (PassthroughCameraPermissions.HasCameraPermission != true)
-                return;
+            if (!m_hasPermission)
+            {
+                if (PassthroughCameraPermissions.HasCameraPermission != true)
+                    return;
 
-            m_hasPermission = true;
-            _ = StartCoroutine(InitializeWebCamTexture());
+                m_hasPermission = true;
+                _ = StartCoroutine(InitializeWebCamTexture());
+            }
         }
-    }
 
-    private IEnumerator InitializeWebCamTexture()
-    {
-        // Check if Passhtrough is present in the scene and is enabled
-        var ptLayer = FindAnyObjectByType<OVRPassthroughLayer>();
-        if (ptLayer == null || !PassthroughCameraUtils.IsPassthroughEnabled())
+        private IEnumerator InitializeWebCamTexture()
         {
-            PCD.DebugMessage(LogType.Error, "Passthrough must be enabled to use the Passthrough Camera API.");
-            yield break;
-        }
+            // Check if Passhtrough is present in the scene and is enabled
+            var ptLayer = FindAnyObjectByType<OVRPassthroughLayer>();
+            if (ptLayer == null || !PassthroughCameraUtils.IsPassthroughEnabled())
+            {
+                DebugLog.Log(LogType.Error, "Passthrough must be enabled to use the Passthrough Camera API.");
+                yield break;
+            }
 
 #if !UNITY_6000_OR_NEWER
-        // There is a bug on Unity 2022 that causes a crash if you don't wait a frame before initializing the WebCamTexture.
-        // Waiting for one frame is important and prevents the bug.
-        yield return new WaitForEndOfFrame();
+            // There is a bug on Unity 2022 that causes a crash if you don't wait a frame before initializing the WebCamTexture.
+            // Waiting for one frame is important and prevents the bug.
+            yield return new WaitForEndOfFrame();
 #endif
 
-        while (true)
-        {
-            var devices = WebCamTexture.devices;
-            if (PassthroughCameraUtils.EnsureInitialized() && PassthroughCameraUtils.CameraEyeToCameraIdMap.TryGetValue(Eye, out var cameraData))
+            while (true)
             {
-                if (cameraData.index < devices.Length)
+                var devices = WebCamTexture.devices;
+                if (PassthroughCameraUtils.EnsureInitialized() &&
+                    PassthroughCameraUtils.CameraEyeToCameraIdMap.TryGetValue(Eye, out var cameraData))
                 {
-                    var deviceName = devices[cameraData.index].name;
-                    WebCamTexture webCamTexture;
-                    if (RequestedResolution == Vector2Int.zero)
-                    {
-                        var largestResolution = PassthroughCameraUtils.GetOutputSizes(Eye).OrderBy(static size => size.x * size.y).Last();
-                        webCamTexture = new WebCamTexture(deviceName, largestResolution.x, largestResolution.y);
-                    }
-                    else
+                    if (cameraData.index < devices.Length)
                     {
-                        webCamTexture = new WebCamTexture(deviceName, RequestedResolution.x, RequestedResolution.y);
+                        var deviceName = devices[cameraData.index].name;
+                        WebCamTexture webCamTexture;
+                        if (RequestedResolution == Vector2Int.zero)
+                        {
+                            var largestResolution = PassthroughCameraUtils.GetOutputSizes(Eye)
+                                .OrderBy(static size => size.x * size.y).Last();
+                            webCamTexture = new WebCamTexture(deviceName, largestResolution.x, largestResolution.y);
+                        }
+                        else
+                        {
+                            webCamTexture = new WebCamTexture(deviceName, RequestedResolution.x, RequestedResolution.y);
+                        }
+
+                        webCamTexture.Play();
+                        var currentResolution = new Vector2Int(webCamTexture.width, webCamTexture.height);
+                        if (RequestedResolution != Vector2Int.zero && RequestedResolution != currentResolution)
+                        {
+                            DebugLog.Log(LogType.Warning,
+                                $"WebCamTexture created, but '{nameof(RequestedResolution)}' {RequestedResolution} is not supported. Current resolution: {currentResolution}.");
+                        }
+
+                        WebCamTexture = webCamTexture;
+                        DebugLog.Log(LogType.Log,
+                            $"WebCamTexture created, texturePtr: {WebCamTexture.GetNativeTexturePtr()}, size: {WebCamTexture.width}/{WebCamTexture.height}");
+
+                        webCamTexture.Stop();
+                        IsReady = true;
+
+                        yield break;
                     }
-                    webCamTexture.Play();
-                    var currentResolution = new Vector2Int(webCamTexture.width, webCamTexture.height);
-                    if (RequestedResolution != Vector2Int.zero && RequestedResolution != currentResolution)
-                    {
-                        PCD.DebugMessage(LogType.Warning, $"WebCamTexture created, but '{nameof(RequestedResolution)}' {RequestedResolution} is not supported. Current resolution: {currentResolution}.");
-                    }
-                    WebCamTexture = webCamTexture;
-                    PCD.DebugMessage(LogType.Log, $"WebCamTexture created, texturePtr: {WebCamTexture.GetNativeTexturePtr()}, size: {WebCamTexture.width}/{WebCamTexture.height}");
-
-                    webCamTexture.Stop();
-                    IsReady = true;
-
-                    yield break;
                 }
-            }
 
-            PCD.DebugMessage(LogType.Error, $"Requested camera is not present in WebCamTexture.devices: {string.Join(", ", devices)}.");
-            yield return null;
+                DebugLog.Log(LogType.Error,
+                    $"Requested camera is not present in WebCamTexture.devices: {string.Join(", ", devices)}.");
+                yield return null;
+            }
         }
     }
-}
 
-/// <summary>
-/// Defines the position of a passthrough camera relative to the headset
-/// </summary>
-public enum PassthroughCameraEye
-{
-    Left,
-    Right
+    /// <summary>
+    /// Defines the position of a passthrough camera relative to the headset
+    /// </summary>
+    public enum PassthroughCameraEye
+    {
+        Left,
+        Right
+    }
 }
diff --git a/package.json b/package.json
index e58fb13df48ae03dffafecc82e6fb51894b5941c..287e423581fdcd9d0bf3313266034446fc99cd04 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "de.tu-dresden.hyper.pca-apriltag",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "displayName": "HYPER PCA AprilTag",
   "description": "This package provides AprilTag detection using Passthrough Camera API.",
   "unity": "6000.0",