# Copyright (c) 2018-2020, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#
import os
import math
import time
import datetime
import omni.ext
import carb
import omni.kit.app
import omni.timeline
import omni.usd
from omni.kit.viewport.utility import get_active_viewport, capture_viewport_to_file
from .capture_options import *
from .capture_progress import *
from .video_generation import VideoGenerationHelper
from .helper import get_num_pattern_file_path


VIDEO_FRAMES_DIR_NAME = "frames"
DEFAULT_IMAGE_FRAME_TYPE_FOR_VIDEO = ".png"
capture_instance = None


class CaptureExtension(omni.ext.IExt):
    def on_startup(self):
        global capture_instance
        capture_instance = self

        self._options = CaptureOptions()
        self._progress = CaptureProgress()
        self._progress_window = CaptureProgressWindow()

        self._renderer = None

        import omni.renderer_capture
        self._renderer = omni.renderer_capture.acquire_renderer_capture_interface()
        self._viewport_api = None

        self._app = omni.kit.app.get_app_interface()
        self._timeline = omni.timeline.get_timeline_interface()
        self._usd_context = omni.usd.get_context()
        self._selection = self._usd_context.get_selection()
        self._settings = carb.settings.get_settings()
        self._show_default_progress_window = True
        self._progress_update_fn = None
        self._forward_one_frame_fn = None
        self._capture_finished_fn = None

        carb.log_warn("Deprecated notice: Please be noted that omni.kit.capture has been replaced with omni.kit.capture.viewport and will be removed in future releases. " \
            "Please update the use of this extension to the new one.")

    def on_shutdown(self):
        self._progress = None
        self._progress_window = None
        global capture_instance
        capture_instance = None

    @property
    def options(self):
        return self._options

    @options.setter
    def options(self, value):
        self._options = value

    @property
    def progress(self):
        return self._progress

    @property
    def show_default_progress_window(self):
        return self._show_default_progress_window

    @show_default_progress_window.setter
    def show_default_progress_window(self, value):
        self._show_default_progress_window = value

    @property
    def progress_update_fn(self):
        return self._progress_update_fn

    @progress_update_fn.setter
    def progress_update_fn(self, value):
        self._progress_update_fn = value

    @property
    def forward_one_frame_fn(self):
        return self._forward_one_frame_fn

    @forward_one_frame_fn.setter
    def forward_one_frame_fn(self, value):
        self._forward_one_frame_fn = value

    @property
    def capture_finished_fn(self):
        return self._capture_finished_fn

    @capture_finished_fn.setter
    def capture_finished_fn(self, value):
        self._capture_finished_fn = value

    def start(self):
        self._prepare_folder_and_counters()
        self._prepare_viewport()
        self._start_internal()

    def pause(self):
        if self._progress.capture_status == CaptureStatus.CAPTURING and self._ui_pause_button.text == "Pause":
            self._progress.capture_status = CaptureStatus.PAUSED

    def resume(self):
        if self._progress.capture_status == CaptureStatus.PAUSED:
            self._progress.capture_status = CaptureStatus.CAPTURING

    def cancel(self):
        if (
            self._progress.capture_status == CaptureStatus.CAPTURING
            or self._progress.capture_status == CaptureStatus.PAUSED
        ):
            self._progress.capture_status = CaptureStatus.CANCELLED

    def _update_progress_hook(self):
        if self._progress_update_fn is not None:
            self._progress_update_fn(
                self._progress.capture_status,
                self._progress.progress,
                self._progress.elapsed_time,
                self._progress.estimated_time_remaining,
                self._progress.current_frame_time,
                self._progress.average_frame_time,
                self._progress.encoding_time,
                self._frame_counter,
                self._total_frame_count,
            )

    def _get_index_for_image(self, dir, file_name, image_suffix):
        def is_int(string_val):
            try:
                v = int(string_val)
                return True
            except:
                return False

        images = os.listdir(dir)
        name_len = len(file_name)
        suffix_len = len(image_suffix)
        max_index = 0
        for item in images:
            if item.startswith(file_name) and item.endswith(image_suffix):
                num_part = item[name_len : (len(item) - suffix_len)]
                if is_int(num_part):
                    num = int(num_part)
                    if max_index < num:
                        max_index = num
        return max_index + 1

    def _float_to_time(self, ft):
        hour = int(ft)
        ft = (ft - hour) * 60
        minute = int(ft)
        ft = (ft - minute) * 60
        second = int(ft)
        ft = (ft - second) * 1000000
        microsecond = int(ft)
        return datetime.time(hour, minute, second, microsecond)

    def _is_environment_sunstudy_player(self):
        if self._options.sunstudy_player is not None:
            return type(self._options.sunstudy_player).__module__ == "omni.kit.environment.core.sunstudy_player.player"
        else:
            carb.log_warn("Sunstudy player type check is valid only when the player is available.")
            return False

    def _update_sunstudy_player_time(self):
        if self._is_environment_sunstudy_player():
            self._options.sunstudy_player.current_time = self._sunstudy_current_time
        else:
            self._options.sunstudy_player.update_time(self._sunstudy_current_time)

    def _set_sunstudy_player_time(self, current_time):
        if self._is_environment_sunstudy_player():
            self._options.sunstudy_player.current_time = current_time
        else:
            date_time = self._options.sunstudy_player.get_date_time()
            time_to_set = self._float_to_time(current_time)
            new_date_time = datetime.datetime(
                date_time.year, date_time.month, date_time.day,
                time_to_set.hour, time_to_set.minute, time_to_set.second
            )
            self._options.sunstudy_player.set_date_time(new_date_time)

    def _prepare_sunstudy_counters(self):
        self._total_frame_count = self._options.fps * self._options.sunstudy_movie_length_in_seconds
        duration = self._options.sunstudy_end_time - self._options.sunstudy_start_time
        self._sunstudy_iterations_per_frame = self._options.ptmb_subframes_per_frame
        self._sunstudy_delta_time_per_iteration = duration / float(self._total_frame_count * self._sunstudy_iterations_per_frame)
        self._sunstudy_current_time = self._options.sunstudy_start_time
        self._set_sunstudy_player_time(self._sunstudy_current_time)

    def _prepare_folder_and_counters(self):
        self._workset_dir = self._options.output_folder
        if not self._make_sure_directory_existed(self._workset_dir):
            carb.log_warn(f"Capture failed due to unable to create folder {self._workset_dir}")
            self._finish()
            return

        if self._options.is_capturing_nth_frames():
            if self._options.capture_every_Nth_frames == 1:
                frames_folder = self._options.file_name + "_frames"
            else:
                frames_folder = self._options.file_name + "_" + str(self._options.capture_every_Nth_frames) + "th_frames"
            self._nth_frames_dir = os.path.join(self._workset_dir, frames_folder)
            if not self._make_sure_directory_existed(self._nth_frames_dir):
                carb.log_warn(f"Capture failed due to unable to create folder {self._nth_frames_dir}")
                self._finish()
                return

        if self._options.is_video():
            self._frames_dir = os.path.join(self._workset_dir, self._options.file_name + "_" + VIDEO_FRAMES_DIR_NAME)
            if not self._make_sure_directory_existed(self._frames_dir):
                carb.log_warn(
                    f"Capture failed due to unable to create folder {self._workset_dir} to save frames of the video."
                )
                self._finish()
                return

            self._video_name = self._options.get_full_path()
            self._frame_pattern_prefix = os.path.join(self._frames_dir, self._options.file_name)

            if self._options.is_capturing_frame():
                self._start_time = float(self._options.start_frame) / self._options.fps
                self._end_time = float(self._options.end_frame + 1) / self._options.fps
                self._time = self._start_time
                self._frame = self._options.start_frame
                self._start_number = self._frame
                self._total_frame_count = round((self._end_time - self._start_time) * self._options.fps)
            else:
                self._start_time = self._options.start_time
                self._end_time = self._options.end_time
                self._time = self._options.start_time
                self._frame = int(self._options.start_time * self._options.fps)
                self._start_number = self._frame
                self._total_frame_count = math.ceil((self._end_time - self._start_time) * self._options.fps)

            if self._options.movie_type == CaptureMovieType.SUNSTUDY:
                self._prepare_sunstudy_counters()
        else:
            if self._options.is_capturing_nth_frames():
                self._frame_pattern_prefix = self._nth_frames_dir
                if self._options.is_capturing_frame():
                    self._start_time = float(self._options.start_frame) / self._options.fps
                    self._end_time = float(self._options.end_frame + 1) / self._options.fps
                    self._time = self._start_time
                    self._frame = self._options.start_frame
                    self._start_number = self._frame
                    self._total_frame_count = round((self._end_time - self._start_time) * self._options.fps)
                else:
                    self._start_time = self._options.start_time
                    self._end_time = self._options.end_time
                    self._time = self._options.start_time
                    self._frame = int(self._options.start_time * self._options.fps)
                    self._start_number = self._frame
                    self._total_frame_count = math.ceil((self._end_time - self._start_time) * self._options.fps)

                if self._options.movie_type == CaptureMovieType.SUNSTUDY:
                    self._prepare_sunstudy_counters()
            else:
                index = self._get_index_for_image(self._workset_dir, self._options.file_name, self._options.file_type)
                self._frame_pattern_prefix = os.path.join(self._workset_dir, self._options.file_name + str(index))
                self._start_time = self._timeline.get_current_time()
                self._end_time = self._timeline.get_current_time()
                self._time = self._timeline.get_current_time()
                self._frame = 1
                self._start_number = self._frame
                self._total_frame_count = 1
        self._subframe = 0
        self._sample_count = 0
        self._frame_counter = 0
        self._real_time_settle_latency_frames_done = 0
        self._last_skipped_frame_path = ""
        self._path_trace_iterations = 0

        self._time_rate = 1.0 / self._options.fps
        self._time_subframe_rate = (
            self._time_rate * (self._options.ptmb_fsc - self._options.ptmb_fso) / self._options.ptmb_subframes_per_frame
        )

    def _prepare_viewport(self):
        viewport_api = get_active_viewport()
        if viewport_api is None:
            return False

        self._record_current_window_status(viewport_api)

        viewport_api.camera_path = self._options.camera
        viewport_api.resolution = (self._options.res_width, self._options.res_height)
        self._settings.set_bool("/persistent/app/captureFrame/viewport", True)
        self._settings.set_bool("/app/captureFrame/setAlphaTo1", not self._options.save_alpha)
        if self._options.file_type == ".exr":
            self._settings.set_bool("/app/captureFrame/hdr", self._options.hdr_output)
        else:
            self._settings.set_bool("/app/captureFrame/hdr", False)
        if self._options.save_alpha:
            self._settings.set_bool("/rtx/post/backgroundZeroAlpha/enabled", True)
            self._settings.set_bool("/rtx/post/backgroundZeroAlpha/backgroundComposite", False)

        self._selection.clear_selected_prim_paths()

        if self._options.render_preset == CaptureRenderPreset.RAY_TRACE:
            self._switch_renderer = not (self._saved_active_render == "rtx" and self._saved_render_mode == "RaytracedLighting")
            if self._switch_renderer:
                viewport_api.set_hd_engine("rtx", "RaytracedLighting")
                carb.log_info("Switching to RayTracing Mode")
        elif self._options.render_preset == CaptureRenderPreset.PATH_TRACE:
            self._switch_renderer = not (self._saved_active_render == "rtx" and self._saved_render_mode == "PathTracing")
            if self._switch_renderer:
                viewport_api.set_hd_engine("rtx", "PathTracing")
                carb.log_info("Switching to PathTracing Mode")
        elif self._options.render_preset == CaptureRenderPreset.IRAY:
            self._switch_renderer = not (self._saved_active_render == "iray" and self._saved_render_mode == "iray")
            if self._switch_renderer:
                viewport_api.set_hd_engine("iray", "iray")
                carb.log_info("Switching to IRay Mode")
        else:
            self._switch_renderer = False
            carb.log_info("Keeping current Render Mode")

        if self._options.debug_material_type == CaptureDebugMaterialType.SHADED:
            self._settings.set_int("/rtx/debugMaterialType", -1)
        elif self._options.debug_material_type == CaptureDebugMaterialType.WHITE:
            self._settings.set_int("/rtx/debugMaterialType", 0)
        else:
            carb.log_info("Keeping current debug mateiral type")

        # set it to 0 to ensure we accumulate as many samples as requested even across subframes (for motion blur)
        self._settings.set_int("/rtx/pathtracing/totalSpp", 0)

        # don't show light and grid during capturing
        self._settings.set_int("/persistent/app/viewport/displayOptions", 0)

        # disable async rendering for capture, otherwise it won't capture images correctly
        self._settings.set_bool("/app/asyncRendering", False)
        self._settings.set_bool("/app/asyncRenderingLowLatency", False)
        # Rendering to some image buffers additionally require explicitly setting `set_capture_sync(True)`, on top of
        # disabling the `/app/asyncRendering` setting. This can otherwise cause images to hold corrupted buffer
        # information by erroneously assuming a complete image buffer is available when only a first partial subframe
        # has been renderer (as in the case of EXR):
        self._renderer.set_capture_sync( self._options.file_type == ".exr" )
        # frames to wait for the async settings above to be ready, they will need to be detected by viewport, and
        # then viewport will notify the renderer not to do async rendering
        self._frames_to_disable_async_rendering = 2

        # Normally avoid using a high /rtx/pathtracing/spp setting since it causes GPU
        # timeouts for large sample counts. But a value larger than 1 can be useful in Multi-GPU setups
        self._settings.set_int(
            "/rtx/pathtracing/spp", min(self._options.spp_per_iteration, self._options.path_trace_spp)
        )

        # Setting resetPtAccumOnlyWhenExternalFrameCounterChanges ensures we control accumulation explicitly
        # by simpling changing the /rtx/externalFrameCounter value
        self._settings.set_bool("/rtx-transient/resetPtAccumOnlyWhenExternalFrameCounterChanges", True)

        if self._options.render_preset == CaptureRenderPreset.IRAY:
            self._settings.set("/rtx/iray/progressive_rendering_max_samples", self._options.path_trace_spp)

        # Enable syncLoads in materialDB and Hydra. This is needed to make sure texture updates finish before we start the rendering    
        self._settings.set("/rtx/materialDb/syncLoads", True)
        self._settings.set("/rtx/hydra/materialSyncLoads", True)
        self._settings.set("/rtx-transient/resourcemanager/texturestreaming/async", False)
        self._settings.set("/rtx-transient/resourcemanager/texturestreaming/streamingBudgetMB", 0)

        return True

    def _show_progress_window(self):
        return (
            self._options.is_video() or self._options.is_capturing_nth_frames() or (self._options.show_single_frame_progress and self._options.is_capturing_pathtracing_single_frame())
        ) and self.show_default_progress_window

    def _start_internal(self):
        # If texture streaming is enabled, set the preroll to at least 1 frame
        if self._settings.get("/rtx-transient/resourcemanager/enableTextureStreaming"):
            self._options.preroll_frames = max(self._options.preroll_frames, 1)

        # set usd time code second to target frame rate
        self._saved_timecodes_per_second = self._timeline.get_time_codes_per_seconds()
        self._timeline.set_time_codes_per_second(self._options.fps)

        # if we want preroll, then set timeline's current time back with the preroll frames' time,
        # and rely on timeline to do the preroll using the give timecode
        if self._options.preroll_frames > 0:
            self._timeline.set_current_time(self._start_time - self._options.preroll_frames / self._options.fps)
            self._settings.set("/iray/current_frame_time", self._start_time - self._options.preroll_frames / self._options.fps)
        else:
            self._timeline.set_current_time(self._start_time)
            self._settings.set("/iray/current_frame_time", self._start_time)

        # change timeline to be in play state
        self._timeline.play()

        # disable automatic time update in timeline so that movie capture tool can control time step
        self._timeline.set_auto_update(False)

        self._update_sub = (
            omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(self._on_update, order=1000000)
        )

        self._progress.start_capturing(self._end_time - self._start_time, self._time_rate, self._options.preroll_frames)
        # always show single frame capture progress for PT mode
        self._options.show_single_frame_progress = True
        if self._show_progress_window():
            self._progress_window.show(
                self._progress,
                self._options.is_capturing_pathtracing_single_frame(),
                show_pt_subframes=self._options.render_preset == CaptureRenderPreset.PATH_TRACE,
                show_pt_iterations=self._options.render_preset == CaptureRenderPreset.IRAY
            )

    def _record_current_window_status(self, viewport_api):
        assert viewport_api is not None, "No viewport to record to"
        self._viewport_api = viewport_api
        self._saved_camera = viewport_api.camera_path
        self._saved_hydra_engine = viewport_api.hydra_engine
        self._saved_render_mode = viewport_api.render_mode
        resolution = viewport_api.resolution

        self._saved_resolution_width = int(resolution[0])
        self._saved_resolution_height = int(resolution[1])
        self._saved_capture_frame_viewport = self._settings.get("/persistent/app/captureFrame/viewport")
        self._saved_active_render = self._settings.get("/renderer/active")
        self._saved_debug_material_type = self._settings.get("/rtx/debugMaterialType")
        self._saved_total_spp = int(self._settings.get("/rtx/pathtracing/totalSpp"))
        self._saved_spp = int(self._settings.get("/rtx/pathtracing/spp"))
        self._saved_reset_pt_accum_only = self._settings.get("/rtx-transient/resetPtAccumOnlyWhenExternalFrameCounterChanges")
        self._saved_display_options = self._settings.get("/persistent/app/viewport/displayOptions")
        self._saved_async_rendering = self._settings.get("/app/asyncRendering")
        self._saved_async_renderingLatency = self._settings.get("/app/asyncRenderingLowLatency")
        self._saved_background_zero_alpha = self._settings.get("/rtx/post/backgroundZeroAlpha/enabled")
        self._saved_background_zero_alpha_comp = self._settings.get("/rtx/post/backgroundZeroAlpha/backgroundComposite")

        if self._options.render_preset == CaptureRenderPreset.IRAY:
            self._saved_iray_sample_limit = int(self._settings.get("/rtx/iray/progressive_rendering_max_samples"))
        if self._options.movie_type == CaptureMovieType.SUNSTUDY:
            self._saved_sunstudy_current_time = self._options.sunstudy_current_time
        self._saved_timeline_current_time = self._timeline.get_current_time()
        self._saved_rtx_sync_load_setting = self._settings.get("/rtx/materialDb/syncLoads")
        self._saved_hydra_sync_load_setting = self._settings.get("/rtx/hydra/materialSyncLoads")
        self._saved_async_texture_streaming = self._settings.get("/rtx-transient/resourcemanager/texturestreaming/async")
        self._saved_texture_streaming_budget = self._settings.get("/rtx-transient/resourcemanager/texturestreaming/streamingBudgetMB")


    def _restore_window_status(self):
        viewport_api, self._viewport_api = self._viewport_api, None
        assert viewport_api is not None, "No viewport to restore to"

        viewport_api.camera_path = self._saved_camera
        viewport_api.resolution = (self._saved_resolution_width, self._saved_resolution_height)
        if self._switch_renderer:
            viewport_api.set_hd_engine(self._saved_hydra_engine, self._saved_render_mode)

        self._settings.set_bool("/persistent/app/captureFrame/viewport", self._saved_capture_frame_viewport)
        self._settings.set_int("/rtx/debugMaterialType", self._saved_debug_material_type)
        self._settings.set_int("/rtx/pathtracing/totalSpp", self._saved_total_spp)
        self._settings.set_int("/rtx/pathtracing/spp", self._saved_spp)
        self._settings.set_bool("/rtx-transient/resetPtAccumOnlyWhenExternalFrameCounterChanges", self._saved_reset_pt_accum_only)
        self._settings.set_int("/persistent/app/viewport/displayOptions", self._saved_display_options)
        self._settings.set_bool("/app/asyncRendering", self._saved_async_rendering)
        self._settings.set_bool("/app/asyncRenderingLowLatency", self._saved_async_renderingLatency)
        self._renderer.set_capture_sync(not self._saved_async_rendering)
        self._settings.set_bool("/rtx/post/backgroundZeroAlpha/enabled", self._saved_background_zero_alpha)
        self._settings.set_bool(
            "/rtx/post/backgroundZeroAlpha/backgroundComposite", self._saved_background_zero_alpha_comp
        )

        if self._options.render_preset == CaptureRenderPreset.IRAY:
            self._settings.set("/rtx/iray/progressive_rendering_max_samples", self._saved_iray_sample_limit)
        if self._options.movie_type == CaptureMovieType.SUNSTUDY:
            self._set_sunstudy_player_time(self._saved_sunstudy_current_time)

        self._settings.set("/rtx/materialDb/syncLoads", self._saved_rtx_sync_load_setting)
        self._settings.set("/rtx/hydra/materialSyncLoads", self._saved_hydra_sync_load_setting)
        self._settings.set("/rtx-transient/resourcemanager/texturestreaming/async", self._saved_async_texture_streaming)
        self._settings.set("/rtx-transient/resourcemanager/texturestreaming/streamingBudgetMB", self._saved_texture_streaming_budget)

        self._settings.set("/app/captureFrame/hdr", False)

    def _clean_pngs_in_directory(self, directory):
        self._clean_files_in_directory(directory, ".png")

    def _clean_files_in_directory(self, directory, suffix):
        images = os.listdir(directory)
        for item in images:
            if item.endswith(suffix):
                os.remove(os.path.join(directory, item))

    def _make_sure_directory_existed(self, directory):
        if not os.path.exists(directory):
            try:
                os.makedirs(directory, exist_ok=True)
            except OSError as error:
                carb.log_warn(f"Directory cannot be created: {dir}")
                return False
        return True

    def _finish(self):
        if (
            self._progress.capture_status == CaptureStatus.FINISHING
            or self._progress.capture_status == CaptureStatus.CANCELLED
        ):
            self._update_sub = None
            self._restore_window_status()
            self._sample_count = 0
            self._start_number = 0
            self._frame_counter = 0
            self._path_trace_iterations = 0
            self._progress.capture_status = CaptureStatus.NONE

            # restore timeline settings
            # stop timeline, but re-enable auto update
            timeline = self._timeline
            timeline.set_auto_update(True)
            timeline.stop()
            timeline.set_time_codes_per_second(self._saved_timecodes_per_second)
            self._timeline.set_current_time(self._saved_timeline_current_time)
            self._settings.set("/iray/current_frame_time", self._saved_timeline_current_time)

            if self._show_progress_window():
                self._progress_window.close()

            if self._capture_finished_fn is not None:
                self._capture_finished_fn()

    def _wait_for_image_writing(self):
        # wait for the last frame is written to disk
        # my tests of scenes of different complexity show a range of 0.2 to 1 seconds wait time
        # so 5 second max time should be enough and we can early quit by checking the last
        # frame every 0.1 seconds.
        SECONDS_TO_WAIT = 5
        SECONDS_EACH_TIME = 0.1
        seconds_tried = 0.0
        carb.log_info("Waiting for frames to be ready for encoding.")
        while seconds_tried < SECONDS_TO_WAIT:
            if os.path.isfile(self._frame_path) and os.access(self._frame_path, os.R_OK):
                break
            else:
                time.sleep(SECONDS_EACH_TIME)
                seconds_tried += SECONDS_EACH_TIME

        if seconds_tried >= SECONDS_TO_WAIT:
            carb.log_warn(f"Wait time out. To start encoding with images already have.")

    def _capture_viewport(self, frame_path: str):
        capture_viewport_to_file(self._viewport_api, file_path=frame_path)
        carb.log_info(f"Capturing {frame_path}")

    def _get_current_frame_output_path(self):
        frame_path = ""
        if self._options.is_video():
            frame_path = get_num_pattern_file_path(
                self._frames_dir,
                self._options.file_name,
                self._options.file_name_num_pattern,
                self._frame,
                DEFAULT_IMAGE_FRAME_TYPE_FOR_VIDEO,
                self._options.renumber_negative_frame_number_from_0,
                abs(self._start_number)
            )
        else:
            if self._options.is_capturing_nth_frames():
                if self._frame_counter % self._options.capture_every_Nth_frames == 0:
                    frame_path = get_num_pattern_file_path(
                        self._frame_pattern_prefix,
                        self._options.file_name,
                        self._options.file_name_num_pattern,
                        self._frame,
                        self._options.file_type,
                        self._options.renumber_negative_frame_number_from_0,
                        abs(self._start_number)
                    )
            else:
                frame_path = self._frame_pattern_prefix + self._options.file_type
        return frame_path

    def _handle_skipping_frame(self, dt):
        if not self._options.overwrite_existing_frames:
            if os.path.exists(self._frame_path):
                carb.log_warn(f"Frame {self._frame_path} exists, skip it...")
                self._settings.set_int("/rtx/pathtracing/spp", 1)
                self._subframe = 0
                self._sample_count = 0
                self._path_trace_iterations = 0
                can_continue = True
                if self._forward_one_frame_fn is not None:
                    can_continue = self._forward_one_frame_fn(dt)
                elif self._options.movie_type == CaptureMovieType.SUNSTUDY and \
                        (self._options.is_video() or self._options.is_capturing_nth_frames()):
                    self._sunstudy_current_time += self._sunstudy_delta_time_per_iteration * self._sunstudy_iterations_per_frame
                    self._update_sunstudy_player_time()
                else:  # movie type is SEQUENCE
                    self._time = self._start_time + (self._frame - self._start_number) * self._time_rate
                    self._timeline.set_current_time(self._time)
                    self._settings.set("/iray/current_frame_time", self._time)
                self._frame += 1
                self._frame_counter += 1

                # check if capture ends
                if self._forward_one_frame_fn is not None:
                    if can_continue is False:
                        if self._options.is_video():
                            self._progress.capture_status = CaptureStatus.TO_START_ENCODING
                        elif self._options.is_capturing_nth_frames():
                            self._progress.capture_status = CaptureStatus.FINISHING
                else:
                    if self._time >= self._end_time or self._frame_counter >= self._total_frame_count:
                        if self._options.is_video():
                            self._progress.capture_status = CaptureStatus.TO_START_ENCODING
                        elif self._options.is_capturing_nth_frames():
                            self._progress.capture_status = CaptureStatus.FINISHING

                return True
        else:
            if os.path.exists(self._frame_path) and self._last_skipped_frame_path != self._frame_path:
                carb.log_warn(f"Frame {self._frame_path} will be overwritten.")
                self._last_skipped_frame_path = self._frame_path
        return False

    def _handle_real_time_capture_settle_latency(self):
        if (self._options.render_preset == CaptureRenderPreset.RAY_TRACE
                and self._options.real_time_settle_latency_frames > 0):
            self._real_time_settle_latency_frames_done += 1
            if self._real_time_settle_latency_frames_done > self._options.real_time_settle_latency_frames:
                self._real_time_settle_latency_frames_done = 0
                return False
            else:
                self._subframe = 0
                self._sample_count = 0
                return True
        return False

    def _on_update(self, e):
        dt = e.payload["dt"]
        if self._progress.capture_status == CaptureStatus.FINISHING:
            self._finish()
            self._update_progress_hook()
        elif self._progress.capture_status == CaptureStatus.CANCELLED:
            # carb.log_warn("video recording cancelled")
            self._update_progress_hook()
            self._finish()
        elif self._progress.capture_status == CaptureStatus.ENCODING:
            if VideoGenerationHelper().encoding_done:
                self._progress.capture_status = CaptureStatus.FINISHING
            self._update_progress_hook()
            self._progress.add_encoding_time(dt)
        elif self._progress.capture_status == CaptureStatus.TO_START_ENCODING:
            if VideoGenerationHelper().is_encoding is False:
                self._wait_for_image_writing()
                if self._options.renumber_negative_frame_number_from_0 is True and self._start_number < 0:
                    video_frame_start_num = 0
                else:
                    video_frame_start_num = self._start_number
                started = VideoGenerationHelper().generating_video(
                    self._video_name,
                    self._frames_dir,
                    self._options.file_name,
                    self._options.file_name_num_pattern,
                    video_frame_start_num,
                    self._total_frame_count,
                    self._options.fps,
                )
                if started:
                    self._progress.capture_status = CaptureStatus.ENCODING
                    self._update_progress_hook()
                    self._progress.add_encoding_time(dt)
                else:
                    carb.log_warn("Movie capture failed to encode the capture images.")
                    self._progress.capture_status = CaptureStatus.FINISHING
        elif self._progress.capture_status == CaptureStatus.CAPTURING:
            if self._frames_to_disable_async_rendering >= 0:
                self._frames_to_disable_async_rendering -= 1
                return

            if self._progress.is_prerolling():
                self._progress.prerolled_frames += 1
                self._settings.set_int("/rtx/pathtracing/spp", 1)
                left_preroll_frames = self._options.preroll_frames - self._progress.prerolled_frames
                self._timeline.set_current_time(self._start_time - left_preroll_frames / self._options.fps)
                self._settings.set("/iray/current_frame_time", self._start_time - left_preroll_frames / self._options.fps)
                return

            self._frame_path = self._get_current_frame_output_path()
            if self._handle_skipping_frame(dt):
                return

            self._settings.set_int(
                "/rtx/pathtracing/spp", min(self._options.spp_per_iteration, self._options.path_trace_spp)
            )

            if self._options.render_preset == CaptureRenderPreset.IRAY:
                iterations_done = int(self._settings.get("/iray/progression"))
                self._path_trace_iterations = iterations_done
                self._sample_count = iterations_done
            else:
                self._sample_count += self._options.spp_per_iteration
            self._timeline.set_prerolling(False)
            self._settings.set_int("/rtx/externalFrameCounter", self._frame)

            # update progress timers
            if self._options.is_capturing_pathtracing_single_frame():
                self._progress.add_single_frame_capture_time(self._subframe, self._options.ptmb_subframes_per_frame, dt)
            else:
                self._progress.add_frame_time(self._frame, dt, self._subframe + 1, self._path_trace_iterations)
                self._update_progress_hook()

            # capture frame when we reach the sample count for this frame and are rendering the last subframe
            # Note _sample_count can go over _samples_per_pixel when 'spp_per_iteration > 1'
            if (self._sample_count >= self._options.path_trace_spp) and (
                self._subframe == self._options.ptmb_subframes_per_frame - 1
            ):
                if self._handle_real_time_capture_settle_latency():
                    return
                if self._options.is_video():
                    self._capture_viewport(self._frame_path)
                else:
                    if self._options.is_capturing_nth_frames():
                        if self._frame_counter % self._options.capture_every_Nth_frames == 0:
                            self._capture_viewport(self._frame_path)
                    else:
                        self._progress.capture_status = CaptureStatus.FINISHING
                        self._capture_viewport(self._frame_path)

            # reset time the *next frame* (since otherwise we capture the first sample)
            if self._sample_count >= self._options.path_trace_spp:
                self._sample_count = 0
                self._path_trace_iterations = 0
                self._subframe += 1
                if self._subframe == self._options.ptmb_subframes_per_frame:
                    self._subframe = 0
                    self._frame += 1
                    self._frame_counter += 1

                self._time = self._start_time + (self._frame - self._start_number) * self._time_rate

                can_continue = False
                if self._forward_one_frame_fn is not None:
                    can_continue = self._forward_one_frame_fn(dt)
                elif self._options.movie_type == CaptureMovieType.SUNSTUDY and \
                        (self._options.is_video() or self._options.is_capturing_nth_frames()):
                    self._sunstudy_current_time += self._sunstudy_delta_time_per_iteration
                    self._update_sunstudy_player_time()
                else:
                    cur_time = (
                        self._time + (self._options.ptmb_fso * self._time_rate) + self._time_subframe_rate * self._subframe
                    )
                    self._timeline.set_current_time(cur_time)
                    self._settings.set("/iray/current_frame_time", cur_time)

                if self._forward_one_frame_fn is not None:
                    if can_continue == False:
                        if self._options.is_video():
                            self._progress.capture_status = CaptureStatus.TO_START_ENCODING
                        elif self._options.is_capturing_nth_frames():
                            self._progress.capture_status = CaptureStatus.FINISHING
                else:
                    if self._time >= self._end_time or self._frame_counter >= self._total_frame_count:
                        if self._options.is_video():
                            self._progress.capture_status = CaptureStatus.TO_START_ENCODING
                        elif self._options.is_capturing_nth_frames():
                            self._progress.capture_status = CaptureStatus.FINISHING

    @staticmethod
    def get_instance():
        global capture_instance
        return capture_instance
