Repository: cvat-ai/cvat
Pull request: Added cached frames indication
<!-- Raise an issue to propose your change (https://github.com/opencv/cvat/issues).
It helps to avoid duplication of efforts from multiple independent contributors.
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
Read the [Contribution guide](https://opencv.github.io/cvat/docs/contributing/). -->

<!-- Provide a general summary of your changes in the Title above -->

Depends on #6585

### Motivation and context
Resolved #8

Decoded range is red:
<img width="537" alt="image" src="https://github.com/opencv/cvat/assets/40690378/686c4016-ef09-4ec9-8815-2c88f820f2d6">

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
If an item isn't applicable for some reason, then ~~explicitly strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the pull request.
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have added a description of my changes into the [CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md) file
- [ ] I have updated the documentation accordingly
- [ ] I have added tests to cover my changes
- [ ] I have linked related issues (see [GitHub docs](
  https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [x] I have increased versions of npm packages if it is necessary
  ([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),
  [cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning),
  [cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning) and
  [cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
  https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the project.
  Feel free to contact the maintainers if that's a concern.


Problem statement:
Video/Image loading status as on youtube
Another question and likely feature suggestion.

When start a job, if I wait long enough, will all the frames be loaded into the browser?
Or, are they loaded on demand as I seek through the video?
Are they cached locally in memory?

I'm working with 4k video and the interface isn't that usable, at least for my current use model, until all frames have been loaded.

Based on the answer above, it would be great to have feedback as to whether the frames have all been loaded or, better, which frames have been loaded.   What I've seen that works well is using a different color on the seek bar for frames that have been loaded.

If they are demand loaded, it would be nice to have a way to force it to load them all (as long as there's enough memory available).

Hints from issue discussion:
> When start a job, if I wait long enough, will all the frames be loaded into the browser?
Or, are they loaded on demand as I seek through the video?

No, they don't. To reduce server load it will try to preload next 500 frames and it will continue preload other frames as soon as necessary. After jump it will start preload next frames from the new position.

> Are they cached locally in memory?
Yes.

> I'm working with 4k video and the interface isn't that usable, at least for my current use model, until all frames have been loaded.

Could you please describe your annotation use case? Another way to optimize your case is to resize the video before uploading to CVAT.

> What I've seen that works well is using a different color on the seek bar for frames that have been loaded.

I like the feature. I will add it into our internal roadmap. It should not be difficult to implement.

> If they are demand loaded, it would be nice to have a way to force it to load them all (as long as there's enough memory available).

Let's understand your use case. If you annotate with a reasonable speed (e.g. several seconds per image) CVAT should be fast enough to preload next frames for you.
We're annotating sports video from stationary cameras.  Sometimes the players and ball are rather small in the resulting video making scaling the video down less desirable.  At the end of the day, we have to balance accuracy of the annotations with the time it takes to download and tag the videos.  I realize I can scale down the video, or compress the frames more, but need to understand the loading and caching strategy to determine the appropriate trade-offs.  How is the frame cache size determined?  Currently, I'm annotating 30s clips.  I'm guessing the segmentation options when creating jobs is so you can break big jobs into smaller ones, to handle related issues.

The model I'm currently using, in interpolation mode, is to step through the video using at a coarse level (c/v keys) labeling the boxes for a given player.  I think that's the best way to get good per player tracks.  Then I use the scroll bar and single step to quickly slide back and forth through the video, stopping when necessary to add more keyframes to the track.

We're still trying to find the most effective way to annotate these videos, but that's what we're doing so far.  I recall in the research that its easier to annotate one object at a time.  I use the filtering functionality to only show the boxes for the current object.  If you have other suggestions or ideas, please share.

Commit message: Removed extra arguments

Patch to review:
diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts
index 158e35821141..2f5b328f81bf 100644
--- a/cvat-core/src/frames.ts
+++ b/cvat-core/src/frames.ts
@@ -582,12 +582,12 @@ export async function findFrame(
     return lastUndeletedFrame;
 }
 
-export function getRanges(jobID): Array<string> {
+export function getCachedChunks(jobID): number[] {
     if (!(jobID in frameDataCache)) {
         return [];
     }
 
-    return frameDataCache[jobID].provider.cachedFrames;
+    return frameDataCache[jobID].provider.cachedChunks(true);
 }
 
 export function clear(jobID: number): void {
diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts
index ea57c1881f1c..5a873ae823c3 100644
--- a/cvat-core/src/session-implementation.ts
+++ b/cvat-core/src/session-implementation.ts
@@ -13,7 +13,7 @@ import {
     getFrame,
     deleteFrame,
     restoreFrame,
-    getRanges,
+    getCachedChunks,
     clear as clearFrames,
     findFrame,
     getContextImage,
@@ -163,9 +163,9 @@ export function implementJob(Job) {
         return result;
     };
 
-    Job.prototype.frames.ranges.implementation = async function () {
-        const rangesData = await getRanges(this.id);
-        return rangesData;
+    Job.prototype.frames.cachedChunks.implementation = async function () {
+        const cachedChunks = await getCachedChunks(this.id);
+        return cachedChunks;
     };
 
     Job.prototype.frames.preview.implementation = async function (this: JobClass): Promise<string> {
@@ -570,21 +570,18 @@ export function implementTask(Task) {
             isPlaying,
             step,
             this.dimension,
+            (chunkNumber, quality) => job.frames.chunk(chunkNumber, quality),
         );
         return result;
     };
 
-    Task.prototype.frames.ranges.implementation = async function () {
-        const rangesData = {
-            decoded: [],
-            buffered: [],
-        };
+    Task.prototype.frames.cachedChunks.implementation = async function () {
+        let chunks = [];
         for (const job of this.jobs) {
-            const { decoded, buffered } = await getRanges(job.id);
-            rangesData.decoded.push(decoded);
-            rangesData.buffered.push(buffered);
+            const cachedChunks = await getCachedChunks(job.id);
+            chunks = chunks.concat(cachedChunks);
         }
-        return rangesData;
+        return Array.from(new Set(chunks));
     };
 
     Task.prototype.frames.preview.implementation = async function (this: TaskClass): Promise<string> {
diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts
index 4f34ed458ccc..cad8b773ae21 100644
--- a/cvat-core/src/session.ts
+++ b/cvat-core/src/session.ts
@@ -210,8 +210,8 @@ function buildDuplicatedAPI(prototype) {
                         prototype.frames.save,
                     );
                 },
-                async ranges() {
-                    const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.ranges);
+                async cachedChunks() {
+                    const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.cachedChunks);
                     return result;
                 },
                 async preview() {
@@ -329,6 +329,7 @@ export class Job extends Session {
     public readonly taskId: number;
     public readonly dimension: DimensionType;
     public readonly dataChunkType: ChunkType;
+    public readonly dataChunkSize: number;
     public readonly bugTracker: string | null;
     public readonly mode: TaskMode;
     public readonly labels: Label[];
@@ -369,7 +370,7 @@ export class Job extends Session {
         delete: CallableFunction;
         restore: CallableFunction;
         save: CallableFunction;
-        ranges: CallableFunction;
+        cachedChunks: CallableFunction;
         preview: CallableFunction;
         contextImage: CallableFunction;
         search: CallableFunction;
@@ -573,7 +574,7 @@ export class Job extends Session {
             delete: Object.getPrototypeOf(this).frames.delete.bind(this),
             restore: Object.getPrototypeOf(this).frames.restore.bind(this),
             save: Object.getPrototypeOf(this).frames.save.bind(this),
-            ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
+            cachedChunks: Object.getPrototypeOf(this).frames.cachedChunks.bind(this),
             preview: Object.getPrototypeOf(this).frames.preview.bind(this),
             search: Object.getPrototypeOf(this).frames.search.bind(this),
             contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
@@ -684,7 +685,7 @@ export class Task extends Session {
         delete: CallableFunction;
         restore: CallableFunction;
         save: CallableFunction;
-        ranges: CallableFunction;
+        cachedChunks: CallableFunction;
         preview: CallableFunction;
         contextImage: CallableFunction;
         search: CallableFunction;
@@ -1101,7 +1102,7 @@ export class Task extends Session {
             delete: Object.getPrototypeOf(this).frames.delete.bind(this),
             restore: Object.getPrototypeOf(this).frames.restore.bind(this),
             save: Object.getPrototypeOf(this).frames.save.bind(this),
-            ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
+            cachedChunks: Object.getPrototypeOf(this).frames.cachedChunks.bind(this),
             preview: Object.getPrototypeOf(this).frames.preview.bind(this),
             contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
             search: Object.getPrototypeOf(this).frames.search.bind(this),
diff --git a/cvat-data/src/ts/cvat-data.ts b/cvat-data/src/ts/cvat-data.ts
index 00768a043512..8d50fe64083d 100644
--- a/cvat-data/src/ts/cvat-data.ts
+++ b/cvat-data/src/ts/cvat-data.ts
@@ -327,17 +327,11 @@ export class FrameDecoder {
         }
     }
 
-    get cachedChunks(): number[] {
-        return Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).sort((a, b) => a - b);
-    }
-
-    get cachedFrames(): string[] {
-        const chunks = Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).sort((a, b) => a - b);
-        return chunks.map((chunk) => {
-            const frames = Object.keys(this.decodedChunks[chunk]).map((frame) => +frame);
-            const min = Math.min(...frames);
-            const max = Math.max(...frames);
-            return `${min}:${max}`;
-        });
+    public cachedChunks(includeInProgress = false): number[] {
+        const chunkIsBeingDecoded = includeInProgress && this.chunkIsBeingDecoded ?
+            Math.floor(this.chunkIsBeingDecoded.start / this.chunkSize) : null;
+        return Object.keys(this.decodedChunks).map((chunkNumber: string) => +chunkNumber).concat(
+            ...(chunkIsBeingDecoded !== null ? [chunkIsBeingDecoded] : []),
+        ).sort((a, b) => a - b);
     }
 }
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 8b7a899b9ec3..2b1217bb67e6 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -581,10 +581,41 @@ export function switchPlay(playing: boolean): AnyAction {
     };
 }
 
-export function confirmCanvasReady(): AnyAction {
+export function confirmCanvasReady(ranges?: string[]): AnyAction {
     return {
         type: AnnotationActionTypes.CONFIRM_CANVAS_READY,
-        payload: {},
+        payload: { ranges },
+    };
+}
+
+export function confirmCanvasReadyAsync(): ThunkAction {
+    return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
+        try {
+            const state: CombinedState = getState();
+            const { instance: job } = state.annotation.job;
+            const chunks = await job.frames.cachedChunks() as number[];
+            const { startFrame, stopFrame, dataChunkSize } = job;
+
+            const ranges = chunks.map((chunk) => (
+                [
+                    Math.max(startFrame, chunk * dataChunkSize),
+                    Math.min(stopFrame, (chunk + 1) * dataChunkSize - 1),
+                ]
+            )).reduce<Array<[number, number]>>((acc, val) => {
+                if (acc.length && acc[acc.length - 1][1] + 1 === val[0]) {
+                    const newMax = val[1];
+                    acc[acc.length - 1][1] = newMax;
+                } else {
+                    acc.push(val as [number, number]);
+                }
+                return acc;
+            }, []).map(([start, end]) => `${start}:${end}`);
+
+            dispatch(confirmCanvasReady(ranges));
+        } catch (error) {
+            // even if error happens here, do not need to notify the users
+            dispatch(confirmCanvasReady());
+        }
     };
 }
 
diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
index 4493ba014f04..df2d3d63381d 100644
--- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx
@@ -24,7 +24,7 @@ import config from 'config';
 import CVATTooltip from 'components/common/cvat-tooltip';
 import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags';
 import {
-    confirmCanvasReady,
+    confirmCanvasReadyAsync,
     dragCanvas,
     zoomCanvas,
     resetCanvas,
@@ -259,7 +259,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
 function mapDispatchToProps(dispatch: any): DispatchToProps {
     return {
         onSetupCanvas(): void {
-            dispatch(confirmCanvasReady());
+            dispatch(confirmCanvasReadyAsync());
         },
         onDragCanvas(enabled: boolean): void {
             dispatch(dragCanvas(enabled));
diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx
index e21a4a138d99..479ad283ede5 100644
--- a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx
+++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx
@@ -16,7 +16,7 @@ import Spin from 'antd/lib/spin';
 
 import {
     activateObject,
-    confirmCanvasReady,
+    confirmCanvasReadyAsync,
     createAnnotationsAsync,
     dragCanvas,
     editShape,
@@ -131,7 +131,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
             dispatch(dragCanvas(enabled));
         },
         onSetupCanvas(): void {
-            dispatch(confirmCanvasReady());
+            dispatch(confirmCanvasReadyAsync());
         },
         onResetCanvas(): void {
             dispatch(resetCanvas());
diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss
index 7d75f57d8f7e..9d31599fe3d5 100644
--- a/cvat-ui/src/components/annotation-page/styles.scss
+++ b/cvat-ui/src/components/annotation-page/styles.scss
@@ -3,7 +3,7 @@
 //
 // SPDX-License-Identifier: MIT
 
-@import '../../base.scss';
+@import '../../base';
 
 .cvat-annotation-page.ant-layout {
     height: 100%;
@@ -126,15 +126,39 @@
     }
 }
 
-.cvat-player-slider {
+.cvat-player-slider.ant-slider {
     width: 350px;
     margin: 0;
+    margin-top: $grid-unit-size * -0.5;
+
+    > .ant-slider-handle {
+        z-index: 100;
+        margin-top: -3.5px;
+    }
+
+    > .ant-slider-track {
+        background: none;
+    }
 
     > .ant-slider-rail {
+        height: $grid-unit-size;
         background-color: $player-slider-color;
     }
 }
 
+.cvat-player-slider-progress {
+    width: 350px;
+    height: $grid-unit-size;
+    position: absolute;
+    top: 0;
+    pointer-events: none;
+
+    > rect {
+        transition: width 0.5s;
+        fill: #1890ff;
+    }
+}
+
 .cvat-player-filename-wrapper {
     max-width: $grid-unit-size * 30;
     max-height: $grid-unit-size * 3;
@@ -221,7 +245,7 @@
 
             .ant-table-thead {
                 > tr > th {
-                    padding: 5px 5px;
+                    padding: $grid-unit-size 0 $grid-unit-size $grid-unit-size * 0.5;
                 }
             }
         }
@@ -446,7 +470,7 @@
             }
 
             .group {
-                background: rgba(216, 233, 250, 0.5);
+                background: rgba(216, 233, 250, 50%);
                 border: 1px solid #d3e0ec;
             }
         }
diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx
index e95d15cf33d0..931693690be1 100644
--- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx
+++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx
@@ -21,6 +21,7 @@ interface Props {
     startFrame: number;
     stopFrame: number;
     playing: boolean;
+    ranges: string;
     frameNumber: number;
     frameFilename: string;
     frameDeleted: boolean;
@@ -47,6 +48,7 @@ function PlayerNavigation(props: Props): JSX.Element {
         deleteFrameShortcut,
         focusFrameInputShortcut,
         inputFrameRef,
+        ranges,
         onSliderChange,
         onInputChange,
         onURLIconClick,
@@ -105,6 +107,19 @@ function PlayerNavigation(props: Props): JSX.Element {
                             value={frameNumber || 0}
                             onChange={onSliderChange}
                         />
+                        {!!ranges && (
+                            <svg className='cvat-player-slider-progress' viewBox='0 0 1000 16' xmlns='http://www.w3.org/2000/svg'>
+                                {ranges.split(';').map((range) => {
+                                    const [start, end] = range.split(':').map((num) => +num);
+                                    const adjustedStart = Math.max(0, start - 1);
+                                    const totalSegments = stopFrame - startFrame;
+                                    const segmentWidth = 1000 / totalSegments;
+                                    const width = Math.max((end - adjustedStart), 1) * segmentWidth;
+                                    const offset = (Math.max((adjustedStart - startFrame), 0) / totalSegments) * 1000;
+                                    return (<rect rx={10} key={start} x={offset} y={0} height={16} width={width} />);
+                                })}
+                            </svg>
+                        )}
                     </Col>
                 </Row>
                 <Row justify='center'>
diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx
index ee800ce68619..7c88063bf217 100644
--- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx
+++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx
@@ -69,6 +69,7 @@ interface Props {
     onRestoreFrame(): void;
     switchNavigationBlocked(blocked: boolean): void;
     jobInstance: any;
+    ranges: string;
 }
 
 export default function AnnotationTopBarComponent(props: Props): JSX.Element {
@@ -77,6 +78,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
         undoAction,
         redoAction,
         playing,
+        ranges,
         frameNumber,
         frameFilename,
         frameDeleted,
@@ -168,6 +170,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
             startFrame={startFrame}
             stopFrame={stopFrame}
             playing={playing}
+            ranges={ranges}
             frameNumber={frameNumber}
             frameFilename={frameFilename}
             frameDeleted={frameDeleted}
diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
index f53c5f61cdbe..3500e4e08ef1 100644
--- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
+++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx
@@ -65,6 +65,7 @@ interface StateToProps {
     normalizedKeyMap: Record<string, string>;
     canvasInstance: Canvas | Canvas3d;
     forceExit: boolean;
+    ranges: string;
     activeControl: ActiveControl;
 }
 
@@ -91,6 +92,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
         annotation: {
             player: {
                 playing,
+                ranges,
                 frame: {
                     data: { deleted: frameIsDeleted },
                     filename: frameFilename,
@@ -142,6 +144,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
         canvasInstance,
         forceExit,
         activeControl,
+        ranges: ranges.length ? ranges.join(';') : '',
     };
 }
 
@@ -638,6 +641,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
             workspace,
             canvasIsReady,
             keyMap,
+            ranges,
             normalizedKeyMap,
             activeControl,
             searchAnnotations,
@@ -766,6 +770,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
                     workspace={workspace}
                     playing={playing}
                     saving={saving}
+                    ranges={ranges}
                     startFrame={startFrame}
                     stopFrame={stopFrame}
                     frameNumber={frameNumber}
diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts
index 71524879f8bd..3b8c5551bf7e 100644
--- a/cvat-ui/src/reducers/annotation-reducer.ts
+++ b/cvat-ui/src/reducers/annotation-reducer.ts
@@ -55,6 +55,7 @@ const defaultState: AnnotationState = {
     job: {
         openTime: null,
         labels: [],
+        groundTruthJobFramesMeta: null,
         requestedId: null,
         groundTruthJobId: null,
         instance: null,
@@ -72,6 +73,7 @@ const defaultState: AnnotationState = {
             delay: 0,
             changeTime: null,
         },
+        ranges: [],
         playing: false,
         frameAngles: [],
         navigationBlocked: false,
@@ -417,8 +419,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
             };
         }
         case AnnotationActionTypes.CONFIRM_CANVAS_READY: {
+            const { ranges } = action.payload;
             return {
                 ...state,
+                player: {
+                    ...state.player,
+                    ranges: ranges || state.player.ranges,
+                },
                 canvas: {
                     ...state.canvas,
                     ready: true,
diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts
index f29b7dff8f94..1af17eb8e280 100644
--- a/cvat-ui/src/reducers/index.ts
+++ b/cvat-ui/src/reducers/index.ts
@@ -695,6 +695,7 @@ export interface AnnotationState {
             delay: number;
             changeTime: number | null;
         };
+        ranges: string[];
         navigationBlocked: boolean;
         playing: boolean;
         frameAngles: number[];


Code review comments:
diff_hunk: @@ -581,10 +581,41 @@ export function switchPlay(playing: boolean): AnyAction {
     };
 }
 
-export function confirmCanvasReady(): AnyAction {
+export function confirmCanvasReady(ranges?: string[]): AnyAction {
     return {
         type: AnnotationActionTypes.CONFIRM_CANVAS_READY,
-        payload: {},
+        payload: { ranges },
+    };
+}
+
+export function confirmCanvasReadyAsync(): ThunkAction {
+    return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
+        try {
+            const state: CombinedState = getState();
+            const { instance: job } = state.annotation.job;
+            const chunks = await job.frames.cachedChunks() as number[];
+            const { startFrame, stopFrame, dataChunkSize } = job;
+
+            const ranges = chunks.map((chunk) => (
+                [
+                    Math.max(startFrame, chunk * dataChunkSize),
+                    Math.min(stopFrame, (chunk + 1) * dataChunkSize - 1),
+                ]
+            )).reduce<Array<[number, number]>>((acc, val) => {
+                if (acc.length && acc[acc.length - 1][1] + 1 === val[0]) {
+                    const newMax = val[1];
+                    acc[acc.length - 1][1] = newMax;
+                } else {
+                    acc.push(val as [number, number]);
+                }
+                return acc;
+            }, []).map(([start, end]) => `${start}:${end}`);
path: cvat-ui/src/actions/annotation-actions.ts
text: @user1:
Is there a point to convert ranges to string and convert them back to numbers in the `PlayerNavigation` component? 

@author:
Yes, there is a reason.
If we do not convert it to string, each time it will be new reference to numbers array. 
That causes extra rerenders.