1# @ohos.multimedia.movingphotoview (MovingPhotoView)
2
3The **MovingPhotoView** component is used to play moving photos and control the playback status.
4
5> **NOTE**
6>
7> - This component is supported since API version 12. Updates will be marked with a superscript to indicate their earliest API version.
8> - Currently, the **MovingPhotoView** component cannot be used in Previewer.
9
10## Modules to Import
11
12```
13import { MovingPhotoView, MovingPhotoViewController, MovingPhotoViewAttribute } from '@kit.MediaLibraryKit';
14```
15
16## MovingPhotoView
17
18> **NOTE**
19>
20> - Currently, live attributes cannot be set.
21> - Currently, **expandSafeArea** in the ArkUI common attribute **ComponentOptions** cannot be set.
22> - When this component is long pressed to trigger playback, the component area is zoomed in to 1.1 times.
23> - This component uses [AVPlayer](../apis-media-kit/_a_v_player.md#avplayer) to play moving photos. A maximum of three [AVPlayers](../apis-media-kit/_a_v_player.md#avplayer) can be used at the same time. Otherwise, frame freezing may occur during the playback.
24
25MovingPhotoView(options: MovingPhotoViewOptions)
26
27**Parameters**
28
29
30| Name | Type                                                 | Mandatory| Description      |
31| ------- | --------------------------------------------------------- | ---- | -------------- |
32| options | [MovingPhotoViewOptions](#movingphotoviewoptions) | Yes  | Moving photo information.|
33
34## MovingPhotoViewOptions
35
36
37| Name     | Type                                                                                        | Mandatory| Description                                                                                                                                       |
38| ----------- | ------------------------------------------------------------------------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
39| movingPhoto | [MovingPhoto](js-apis-photoAccessHelper.md#movingphoto12) | Yes  | **MovingPhoto** instance. For details, see [MovingPhoto](js-apis-photoAccessHelper.md#movingphoto12).<br>**Atomic service API**: This API can be used in atomic services since API version 12.|
40| controller  | [MovingPhotoViewController](#movingphotoviewcontroller)                                          | No  | Controller used to control the playback status of the moving photo.<br>**Atomic service API**: This API can be used in atomic services since API version 12.                     |
41| imageAIOptions<sup>14+</sup>  | [ImageAIOptions](../apis-arkui/arkui-ts/ts-image-common.md#imageaioptions) | No  | AI options. You can set the image analyzer type or bind an image analyzer controller.<br>**Atomic service API**: This API can be used in atomic services since API version 14.|
42
43## Properties
44
45In addition to the [universal properties](../apis-arkui/arkui-ts/ts-universal-attributes-size.md), the following properties are supported.
46
47### muted
48
49muted(isMuted: boolean)
50
51Sets whether to mute the player.
52
53**Atomic service API**: This API can be used in atomic services since API version 12.
54
55**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
56
57**Parameters**
58
59
60| Name | Type   | Mandatory| Description                        |
61| ------- | ------- | ---- | ---------------------------- |
62| isMuted | boolean | Yes  | Whether to mute the player.<br>Default value: **false**<br>The value **true** means to mute the player; the value **false** means the opposite.|
63
64### objectFit
65
66objectFit(value: ImageFit)
67
68Sets the display mode of the moving photo.
69
70**Atomic service API**: This API can be used in atomic services since API version 12.
71
72**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
73
74**Parameters**
75
76
77| Name| Type                                                                         | Mandatory| Description                            |
78| ------ | ----------------------------------------------------------------------------- | ---- | -------------------------------- |
79| value  | [ImageFit](../apis-arkui/arkui-ts/ts-appendix-enums.md#imagefit) | Yes  | Image scale type.<br>Default value: **Cover**|
80
81### autoPlayPeriod<sup>13+</sup>
82
83autoPlayPeriod(startTime: number, endTime: number)
84
85Sets the autoplay period, which is a configuration item of **autoPlay**.
86
87Before this API is called, [autoPlay](#autoplay13) must be set to **true**. Otherwise, the specified video play period (**startTime**, **endTime**) does not take effect.
88
89**Atomic service API**: This API can be used in atomic services since API version 13.
90
91**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
92
93**Parameters**
94
95
96| Name | Type   | Mandatory| Description                        |
97| ------- | ------- | ---- | ---------------------------- |
98| startTime| number| Yes  | Start playback time, in ms.<br>Value range: [0,3000].|
99| endTime| number| Yes  | End playback time, in ms.<br>Value range: [0,3000].|
100
101### autoPlay<sup>13+</sup>
102
103autoPlay(isAutoPlay: boolean)
104
105Sets autoplay. After the moving photo is played once, a static image is displayed.
106
107**Atomic service API**: This API can be used in atomic services since API version 13.
108
109**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
110
111**Parameters**
112
113
114| Name | Type   | Mandatory| Description                        |
115| ------- | ------- | ---- | ---------------------------- |
116| isAutoPlay| boolean| Yes  | Whether to enable autoplay.<br>The value **true** means to enable autoplay; the value **false** means the opposite.<br>Default value: **false**|
117
118### repeatPlay<sup>13+</sup>
119
120repeatPlay(isRepeatPlay: boolean)
121
122Sets repeat play. **repeatPlay** is mutually exclusive with **autoPlay** and **Long Press**, and takes precedence over them.
123
124**Atomic service API**: This API can be used in atomic services since API version 13.
125
126**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
127
128**Parameters**
129
130
131| Name | Type   | Mandatory| Description                        |
132| ------- | ------- | ---- | ---------------------------- |
133| isRepeatPlay| boolean| Yes  | Whether to enable repeat play.<br>The value **true** means to enable repeat play; the value **false** means the opposite.<br>Default value: **false**|
134
135### enableAnalyzer<sup>14+</sup>
136
137enableAnalyzer(enabled: boolean)
138
139Sets the AI analyzer. Currently, the AI analyzer supports features, such as subject recognition, text recognition, and object search.
140
141**Atomic service API**: This API can be used in atomic services since API version 14.
142
143**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
144
145**Parameters**
146
147
148| Name | Type   | Mandatory| Description                        |
149| ------- | ------- | ---- | ---------------------------- |
150| enabled| boolean| Yes  | Whether to enable the AI analyzer.<br>The value **true** means to enable the AI analyzer, and **false** means the opposite.<br>Default value: **true**|
151
152## Events
153
154In addition to [universal events](../apis-arkui/arkui-ts/ts-universal-events-click.md), the following events are supported.
155
156### onComplete<sup>13+</sup>
157
158onComplete(callback: MovingPhotoViewEventCallback)
159
160Called when the image of a moving photo is loaded.
161
162**Atomic service API**: This API can be used in atomic services since API version 13.
163
164**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
165
166**Parameters**
167
168
169| Name  | Type                                                         | Mandatory| Description                          |
170| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
171| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when the image of a moving photo is loaded.|
172
173### onStart
174
175onStart(callback: MovingPhotoViewEventCallback)
176
177Called when a moving photo starts playing.
178
179**Atomic service API**: This API can be used in atomic services since API version 12.
180
181**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
182
183**Parameters**
184
185
186| Name  | Type                                                         | Mandatory| Description                          |
187| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
188| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when a moving photo starts playing.|
189
190### onPause
191
192onPause(callback: MovingPhotoViewEventCallback)
193
194Called when the playback is paused.
195
196**Atomic service API**: This API can be used in atomic services since API version 12.
197
198**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
199
200**Parameters**
201
202
203| Name  | Type                                                         | Mandatory| Description                          |
204| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
205| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when the playback of a moving photo is paused.|
206
207### onFinish
208
209onFinish(callback: MovingPhotoViewEventCallback)
210
211Called when the playback is finished.
212
213**Atomic service API**: This API can be used in atomic services since API version 12.
214
215**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
216
217**Parameters**
218
219
220| Name  | Type                                                         | Mandatory| Description                          |
221| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
222| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when the playback of a moving photo ends.|
223
224### onError
225
226onError(callback: MovingPhotoViewEventCallback)
227
228Called when the playback fails.
229
230**Atomic service API**: This API can be used in atomic services since API version 12.
231
232**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
233
234**Parameters**
235
236
237| Name  | Type                                                         | Mandatory| Description                          |
238| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
239| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when the playback of a moving photo fails.|
240
241### onStop
242
243onStop(callback: MovingPhotoViewEventCallback)
244
245Called when the playback is stopped by **stop()**.
246
247**Atomic service API**: This API can be used in atomic services since API version 12.
248
249**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
250
251**Parameters**
252
253
254| Name  | Type                                                         | Mandatory| Description                          |
255| -------- | ------------------------------------------------------------- | ---- | ------------------------------ |
256| callback | [MovingPhotoViewEventCallback](#movingphotovieweventcallback) | Yes  | Callback to be invoked when the playback of a moving photo is stopped.|
257
258## MovingPhotoViewEventCallback
259
260declare type MovingPhotoViewEventCallback = () => void
261
262Defines a callback to be invoked when the playback status of a moving photo changes.
263
264## MovingPhotoViewController
265
266A **MovingPhotoViewController** object can be used to control a **MovingPhotoView** component. For details, see [@ohos.multimedia.media](../apis-media-kit/js-apis-media.md).
267
268### startPlayback
269
270startPlayback(): void
271
272Starts playback.
273
274**Atomic service API**: This API can be used in atomic services since API version 12.
275
276**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
277
278### stopPlayback
279
280stopPlayback(): void
281
282Stops playback. Once started again, the playback starts from the beginning.
283
284**Atomic service API**: This API can be used in atomic services since API version 12.
285
286**System capability**: SystemCapability.FileManagement.PhotoAccessHelper.Core
287
288## Example 1: Play a moving photo in multiple modes.
289
290```ts
291// xxx.ets
292import { photoAccessHelper } from '@kit.MediaLibraryKit';
293import { emitter } from '@kit.BasicServicesKit';
294import { dataSharePredicates } from '@kit.ArkData';
295import { MovingPhotoView, MovingPhotoViewController, MovingPhotoViewAttribute } from '@kit.MediaLibraryKit';
296
297const PHOTO_SELECT_EVENT_ID: number = 80001
298
299@Entry
300@Component
301struct MovingPhotoViewDemo {
302  @State src: photoAccessHelper.MovingPhoto | undefined = undefined
303  @State isMuted: boolean = false
304  controller: MovingPhotoViewController = new MovingPhotoViewController()
305
306  aboutToAppear(): void {
307    emitter.on({
308      eventId: PHOTO_SELECT_EVENT_ID,
309      priority: emitter.EventPriority.IMMEDIATE,
310    }, (eventData: emitter.EventData) => {
311      this.src = AppStorage.get<photoAccessHelper.MovingPhoto>('mv_data') as photoAccessHelper.MovingPhoto
312    })
313  }
314
315  aboutToDisappear(): void {
316    emitter.off(PHOTO_SELECT_EVENT_ID)
317  }
318
319  build() {
320    Column() {
321      Row() {
322        Button('PICK')
323          .margin(5)
324          .onClick(async () => {
325            let context = getContext(this)
326            try {
327              let uris: Array<string> = []
328              const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions()
329              photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE
330              photoSelectOptions.maxSelectNumber = 2
331              const photoViewPicker = new photoAccessHelper.PhotoViewPicker()
332              let photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions)
333              uris = photoSelectResult.photoUris
334              if (uris[0]) {
335                this.handlePickerResult(context, uris[0], new MediaDataHandlerMovingPhoto())
336              }
337            } catch (e) {
338              console.error(`pick file failed`)
339            }
340          })
341      }
342      .alignItems(VerticalAlign.Center)
343      .justifyContent(FlexAlign.Center)
344      .height('15%')
345
346      Row() {
347        Column() {
348          MovingPhotoView({
349            movingPhoto: this.src,
350            controller: this.controller
351          })
352            .width('100%')
353            .height('100%')
354            .muted(this.isMuted)
355            .autoPlay(true)
356            .repeatPlay(false)
357            .autoPlayPeriod(0, 600)
358            .objectFit(ImageFit.Cover)
359            .onComplete(() => {
360              console.log('Completed');
361            })
362            .onStart(() => {
363              console.log('onStart')
364            })
365            .onFinish(() => {
366              console.log('onFinish')
367            })
368            .onStop(() => {
369              console.log('onStop')
370            })
371            .onError(() => {
372              console.log('onError')
373            })
374        }
375      }
376      .height('70%')
377
378      Row() {
379        Button('start')
380          .onClick(() => {
381            this.controller.startPlayback()
382          })
383          .margin(5)
384        Button('stop')
385          .onClick(() => {
386            this.controller.stopPlayback()
387          })
388          .margin(5)
389        Button('mute')
390          .onClick(() => {
391            this.isMuted = !this.isMuted
392          })
393          .margin(5)
394      }
395      .alignItems(VerticalAlign.Center)
396      .justifyContent(FlexAlign.Center)
397      .height('15%')
398    }
399  }
400
401  async handlePickerResult(context: Context, uri: string, handler: photoAccessHelper.MediaAssetDataHandler<photoAccessHelper.MovingPhoto>): Promise<void> {
402    let uriPredicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
403    uriPredicates.equalTo('uri', uri)
404    let fetchOptions: photoAccessHelper.FetchOptions = {
405      fetchColumns: [],
406      predicates: uriPredicates
407    };
408    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context)
409    let assetResult = await phAccessHelper.getAssets(fetchOptions)
410    let asset = await assetResult.getFirstObject()
411    let requestOptions: photoAccessHelper.RequestOptions = {
412      deliveryMode: photoAccessHelper.DeliveryMode.FAST_MODE,
413    }
414    try {
415      photoAccessHelper.MediaAssetManager.requestMovingPhoto(context, asset, requestOptions, handler)
416    } catch (err) {
417      console.error("request error: ", err)
418    }
419  }
420}
421
422class MediaDataHandlerMovingPhoto implements photoAccessHelper.MediaAssetDataHandler<photoAccessHelper.MovingPhoto> {
423  async onDataPrepared(movingPhoto: photoAccessHelper.MovingPhoto) {
424    AppStorage.setOrCreate('mv_data', movingPhoto)
425    emitter.emit({
426      eventId: PHOTO_SELECT_EVENT_ID,
427      priority: emitter.EventPriority.IMMEDIATE,
428    }, {
429    })
430  }
431}
432```
433## Example 2: Enable the AI analyzer.
434
435```ts
436// xxx.ets
437import { photoAccessHelper } from '@kit.MediaLibraryKit';
438import { emitter } from '@kit.BasicServicesKit';
439import { dataSharePredicates } from '@kit.ArkData';
440import { MovingPhotoView, MovingPhotoViewController, MovingPhotoViewAttribute } from '@kit.MediaLibraryKit';
441import visionImageAnalyzer from '@hms.ai.visionImageAnalyzer';
442const PHOTO_SELECT_EVENT_ID: number = 80001
443
444@Entry
445@Component
446struct MovingPhotoViewDemo {
447  @State src: photoAccessHelper.MovingPhoto | undefined = undefined
448  @State isMuted: boolean = false
449  controller: MovingPhotoViewController = new MovingPhotoViewController()
450  private aiController: visionImageAnalyzer.VisionImageAnalyzerController =
451    new visionImageAnalyzer.VisionImageAnalyzerController()
452  private options: ImageAIOptions = {
453    types: [ImageAnalyzerType.SUBJECT, ImageAnalyzerType.TEXT, ImageAnalyzerType.OBJECT_LOOKUP],
454    aiController: this.aiController
455  }
456
457  aboutToAppear(): void {
458    emitter.on({
459      eventId: PHOTO_SELECT_EVENT_ID,
460      priority: emitter.EventPriority.IMMEDIATE,
461    }, (eventData: emitter.EventData) => {
462      this.src = AppStorage.get<photoAccessHelper.MovingPhoto>('mv_data') as photoAccessHelper.MovingPhoto
463    })
464  }
465
466  aboutToDisappear(): void {
467    emitter.off(PHOTO_SELECT_EVENT_ID)
468  }
469
470  build() {
471    Column() {
472      Row() {
473        Button('PICK')
474          .margin(5)
475          .onClick(async () => {
476            let context = getContext(this)
477            try {
478              let uris: Array<string> = []
479              const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions()
480              photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE
481              photoSelectOptions.maxSelectNumber = 2
482              const photoViewPicker = new photoAccessHelper.PhotoViewPicker()
483              let photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions)
484              uris = photoSelectResult.photoUris
485              if (uris[0]) {
486                this.handlePickerResult(context, uris[0], new MediaDataHandlerMovingPhoto())
487              }
488            } catch (e) {
489              console.error(`pick file failed`)
490            }
491          })
492      }
493      .alignItems(VerticalAlign.Center)
494      .justifyContent(FlexAlign.Center)
495      .height('15%')
496
497      Row() {
498        Column() {
499          MovingPhotoView({
500            movingPhoto: this.src,
501            controller: this.controller,
502            imageAIOptions: this.options
503          })
504            .width('100%')
505            .height('100%')
506            .muted(this.isMuted)
507            .autoPlay(true)
508            .repeatPlay(false)
509            .autoPlayPeriod(0, 600)
510            .objectFit(ImageFit.Cover)
511            .enableAnalyzer(true)
512            .onComplete(() => {
513              console.log('Completed');
514            })
515            .onStart(() => {
516              console.log('onStart')
517            })
518            .onFinish(() => {
519              console.log('onFinish')
520            })
521            .onStop(() => {
522              console.log('onStop')
523            })
524            .onError(() => {
525              console.log('onError')
526            })
527        }
528      }
529      .height('70%')
530
531      Row() {
532        Button('start')
533          .onClick(() => {
534            this.controller.startPlayback()
535          })
536          .margin(5)
537        Button('stop')
538          .onClick(() => {
539            this.controller.stopPlayback()
540          })
541          .margin(5)
542        Button('mute')
543          .onClick(() => {
544            this.isMuted = !this.isMuted
545          })
546          .margin(5)
547      }
548      .alignItems(VerticalAlign.Center)
549      .justifyContent(FlexAlign.Center)
550      .height('15%')
551    }
552  }
553
554  async handlePickerResult(context: Context, uri: string, handler: photoAccessHelper.MediaAssetDataHandler<photoAccessHelper.MovingPhoto>): Promise<void> {
555    let uriPredicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
556    uriPredicates.equalTo('uri', uri)
557    let fetchOptions: photoAccessHelper.FetchOptions = {
558      fetchColumns: [],
559      predicates: uriPredicates
560    };
561    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context)
562    let assetResult = await phAccessHelper.getAssets(fetchOptions)
563    let asset = await assetResult.getFirstObject()
564    let requestOptions: photoAccessHelper.RequestOptions = {
565      deliveryMode: photoAccessHelper.DeliveryMode.FAST_MODE,
566    }
567    try {
568      photoAccessHelper.MediaAssetManager.requestMovingPhoto(context, asset, requestOptions, handler)
569    } catch (err) {
570      console.error("request error: ", err)
571    }
572  }
573}
574
575class MediaDataHandlerMovingPhoto implements photoAccessHelper.MediaAssetDataHandler<photoAccessHelper.MovingPhoto> {
576  async onDataPrepared(movingPhoto: photoAccessHelper.MovingPhoto) {
577    AppStorage.setOrCreate('mv_data', movingPhoto)
578    emitter.emit({
579      eventId: PHOTO_SELECT_EVENT_ID,
580      priority: emitter.EventPriority.IMMEDIATE,
581    }, {
582    })
583  }
584}
585```
586## Example 3: Use moving photos in an atomic service.
587
588```ts
589// xxx.ets
590import { photoAccessHelper, MovingPhotoView, MovingPhotoViewController, MovingPhotoViewAttribute } from '@kit.MediaLibraryKit';
591
592let context = getContext(this)
593let data: photoAccessHelper.MovingPhoto
594async function loading() {
595  try {
596    // Ensure that the media assets corresponding to imageFileUri and videoFileUri exist in the application sandbox directory.
597    let imageFileUri = 'file://{bundleName}/data/storage/el2/base/haps/entry/files/xxx.jpg';
598    let videoFileUri = 'file://{bundleName}/data/storage/el2/base/haps/entry/files/xxx.mp4';
599    data = await photoAccessHelper.MediaAssetManager.loadMovingPhoto(context, imageFileUri, videoFileUri);
600    console.info('load moving photo successfully');
601  } catch (err) {
602    console.error(`load moving photo failed with error: ${err.code}, ${err.message}`);
603  }
604}
605@Entry
606@Component
607struct Index {
608  controller: MovingPhotoViewController = new MovingPhotoViewController()
609  @State ImageFit: ImageFit | undefined | null = ImageFit.Contain;
610  @State flag: boolean = true;
611  @State autoPlayFlag: boolean = true;
612  @State repeatPlayFlag: boolean = false;
613  @State autoPlayPeriodStart: number = 0;
614  @State autoPlayPeriodEnd: number = 500;
615  aboutToAppear(): void {
616    loading()
617  }
618
619  build() {
620    NavDestination() {
621      Column() {
622        Stack({ alignContent: Alignment.BottomStart }) {
623          MovingPhotoView({
624            movingPhoto: data,
625            controller: this.controller
626          })
627            .width(300)
628            .height(400)
629            .muted(this.flag)
630            .objectFit(this.ImageFit)
631            .autoPlay(this.autoPlayFlag)
632            .autoPlayPeriod(this.autoPlayPeriodStart, this.autoPlayPeriodEnd)
633            .repeatPlay(this.repeatPlayFlag)
634            .onComplete(() => {
635              console.info('onComplete')
636            })
637            .onStart(() => {
638              console.info('onStart')
639            })
640            .onStop(() => {
641              console.info('onStop')
642            })
643            .onPause(() => {
644              console.info('onPause')
645            })
646            .onFinish(() => {
647              console.info('onFinish')
648            })
649            .onError(() => {
650              console.info('onError')
651            })
652        }
653
654        Row() {
655          Button('Play')
656            .onClick(() => {
657              this.controller.startPlayback()
658            })
659          Button('StopPlay')
660            .onClick(() => {
661              this.controller.stopPlayback()
662            })
663          Button('mute').id('MovingPhotoView_true')
664            .onClick(() => {
665              this.flag = false
666            })
667        }
668      }
669    }
670  }
671}
672```
673