如何实现动态照片压缩
==========

#### 问题现象

使用系统相机拍摄的动态图片，使用packToFile编码压缩后会变成静态图片。如何压缩动态图片并维持其动态特性？

#### 背景知识

*   [动态照片](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/photoaccesshelper-movingphoto)：是一种结合了图片和视频的照片形式，可以显示一小段时间的动态画面和声音。图片格式为JPG，视频格式为MP4。
*   [packToFile](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagepacker#packtofile11)：指定编码参数对图片进行编码压缩。
*   [AVTranscoder](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avtranscoder)：视频转码管理，可用于视频压缩。
*   [MovingPhotoView](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/movingphotoview-guidelines)：用于播放动态照片的组件。

#### 解决方案

1.  获取媒体库动态照片对象，将动态照片的图片内容和视频内容分别保存在沙箱文件中。具体可参考：[获取媒体库动态照片对象](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/photoaccesshelper-movingphoto#获取媒体库动态照片对象)。
    
    async function pickMovingPhoto(phAccessHelper: photoAccessHelper.PhotoAccessHelper, context: Context) {
      try {
        _// picker选择动态照片uri。_
        let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
        photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.MOVING\_PHOTO\_IMAGE\_TYPE;
        photoSelectOptions.maxSelectNumber = 9;
        let photoViewPicker = new photoAccessHelper.PhotoViewPicker();
        let photoSelectResult = await photoViewPicker.select(photoSelectOptions);
        let uris = photoSelectResult.photoUris;
        for (let i = 0; i < uris.length; i++) {
         _ // 获取uri对应的PhotoAsset资产。_
          let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
          predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uris\[i\]);
          let fetchOption: photoAccessHelper.FetchOptions = {
            fetchColumns: \[\],
            predicates: predicates,
          };
          let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> =
            await phAccessHelper.getAssets(fetchOption);
          let photoAsset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();
          _// 获取PhotoAsset对应的动态照片对象。_
          await photoAccessHelper.MediaAssetManager.requestMovingPhoto(context, photoAsset, {
            deliveryMode: photoAccessHelper.DeliveryMode.FAST\_MODE,
          }, {
            async onDataPrepared(movingPhoto: photoAccessHelper.MovingPhoto) {
             _ // 保存动态图片图片部分的沙箱文件地址_
              let imageFileUri = 'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image.jpg';
            _  // 保存动态图片视频部分的沙箱文件地址_
              let videoFileUri = 'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video.mp4';
              let imageFile: fileIo.File | undefined;
              let videoFile: fileIo.File | undefined;
              if (movingPhoto !== undefined) {
                console.info(\`request moving photo successfully, uri: ${movingPhoto.getUri()}\`);
                try {
                  fileIo.openSync(imageFileUri, fileIo.OpenMode.CREATE);
                  fileIo.openSync(videoFileUri, fileIo.OpenMode.CREATE);
                  await movingPhoto.requestContent(imageFileUri, videoFileUri);
                } catch (err) {
                  console.error(\`failed to moving photo\`);
                } finally {
                  if (imageFile) {
                    fileIo.closeSync(imageFile);
                  }
                  if (videoFile) {
                    fileIo.closeSync(videoFile);
                  }
                }
              }
            },
          });
        }
      } catch (err) {
        console.error(\`request moving photo failed with error: ${err.code}, ${err.message}\`);
      }
    }
    
2.  使用[ImagePacker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagepacker)类的[packToFile](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagepacker#packtofile11)接口压缩动态照片的图片内容。具体可参考：[图片编码进文件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/image-encoding#图片编码进文件)。
    
    Button('Pack Image')
      .width('100%')
      .onClick(async () => {
       _ // 保存动态照片图片内容的沙箱文件地址_
        let imageFileUri =
          'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image.jpg';
       _ // 保存压缩后的图片文件的沙箱文件地址_
        let packFileUri =
          'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
        let imageFile: fileIo.File | undefined;
        let packFile: fileIo.File | undefined;
        try {
          imageFile = fileIo.openSync(imageFileUri, fileIo.OpenMode.READ\_WRITE);
          packFile = fileIo.openSync(packFileUri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ\_WRITE);
          let imageSource = image.createImageSource(imageFile.fd);
          let imagePacker = image.createImagePacker();
          let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 50 };
          await imagePacker.packToFile(imageSource, packFile.fd, packOpts);
        } catch (err) {
          console.error(\`failed to pack image file: ${JSON.stringify(err)}\`);
        } finally {
          if (imageFile) {
            fileIo.closeSync(imageFile);
          }
          if (packFileUri) {
            fileIo.closeSync(packFile);
          }
        }
      });
    
3.  使用[AVTranscoder](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avtranscoder)对动态照片的视频内容进行编码转换压缩。具体可参考：[使用AVTranscoder实现视频转码](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-avtranscoder-for-transcodering)。
    
    Button('Pack Video')
      .width('100%')
      .onClick(async () => {
        **// 保存动态照片视频内容的沙箱文件地址**
        let videoFileUri =
          'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video.mp4';
      _  // 保存压缩后的视频文件的沙箱文件地址_
        let packFileUri =
          'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
        let videoFile: fileIo.File | undefined;
        let packFile: fileIo.File | undefined;
        try {
          videoFile = fileIo.openSync(videoFileUri, fileIo.OpenMode.READ\_WRITE);
          packFile = fileIo.openSync(packFileUri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ\_WRITE);
          let videoFileSize = fileIo.statSync(videoFile.fd).size;
          this.avTranscoder!.fdSrc = { fd: videoFile.fd, offset: 0, length: videoFileSize };
          this.avTranscoder!.fdDst = packFile.fd;
          let config: media.AVTranscoderConfig = {
            audioCodec: media.CodecMimeType.AUDIO\_AAC,
            videoCodec: media.CodecMimeType.VIDEO\_HEVC,
            fileFormat: media.ContainerFormatType.CFT\_MPEG\_4,
            audioBitrate: 20000,
            videoBitrate: 10000000,
          };
          await this.avTranscoder!.prepare(config);
          await this.avTranscoder!.start();
        } catch (err) {
          console.error(\`failed to pack video file: ${JSON.stringify(err)}\`);
        } finally {
          if (videoFile) {
            fileIo.closeSync(videoFile);
          }
          if (packFile) {
            fileIo.closeSync(packFile);
          }
        }
      });
    
4.  根据压缩后的图片和视频文件重新加载成MovingPhoto对象。具体可参考：[获取应用沙箱动态照片对象](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/photoaccesshelper-movingphoto#获取应用沙箱动态照片对象)。
    
    Button('Load Moving Photo')
      .width('100%')
      .onClick(async () => {
        let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
        try {
          _// 压缩后的图片文件的沙箱地址_
          let imageFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
         _ // 压缩后的视频文件的沙箱地址_
          let videoFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
          this.src =
            await photoAccessHelper.MediaAssetManager.loadMovingPhoto(context, imageFileUri, videoFileUri);
          console.info(\`Load moving photo successfully\`);
        } catch (err) {
          console.error(\`Load Moving photo failed: ${JSON.stringify(err)}\`);
        }
      });
    
5.  根据压缩后的图片和视频文件保存动态照片对象到系统图库。具体可参考：[保存动态照片资源](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/photoaccesshelper-movingphoto#保存动态照片资源)。
    
    SaveButton({
      icon: SaveIconStyle.FULL\_FILLED,
      text: SaveDescription.SAVE\_IMAGE,
      buttonType: ButtonType.Capsule,
    }) _// 创建安全控件按钮。_
      .onClick(async (\_event, result: SaveButtonOnClickResult) => {
        if (result === SaveButtonOnClickResult.SUCCESS) {
          try {
            let context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
            let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
           _ // 压缩后的图片文件的沙箱地址_
            let imageFileUri =
              'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
          _  // 压缩后的视频文件的沙箱地址_
            let videoFileUri =
              'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
            let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
              photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context,
                photoAccessHelper.PhotoType.IMAGE, 'jpg', {
                  title: 'moving\_photo',
                  subtype: photoAccessHelper.PhotoSubtype.MOVING\_PHOTO,
                });
            assetChangeRequest.addResource(photoAccessHelper.ResourceType.IMAGE\_RESOURCE, imageFileUri);
            assetChangeRequest.addResource(photoAccessHelper.ResourceType.VIDEO\_RESOURCE, videoFileUri);
            await phAccessHelper.applyChanges(assetChangeRequest);
            console.info(\`create moving photo successfully, uri: ${assetChangeRequest.getAsset().uri}\`);
          } catch (err) {
            console.error(\`create moving photo failed with error: ${err.code}, ${err.message}\`);
          }
        } else {
          console.error('SaveButtonOnClickResult create moving photo failed');
        }
      });
    

完整代码如下：

import {
  MovingPhotoView,
  MovingPhotoViewController,
  photoAccessHelper,
} from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import image from '@ohos.multimedia.image';
import { fileIo } from '@kit.CoreFileKit';
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { dataSharePredicates } from '@kit.ArkData';


async function pickMovingPhoto(phAccessHelper: photoAccessHelper.PhotoAccessHelper, context: Context) {
  try {
    _// picker选择动态照片uri。_
    let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.MOVING\_PHOTO\_IMAGE\_TYPE;
    photoSelectOptions.maxSelectNumber = 9;
    let photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    let photoSelectResult = await photoViewPicker.select(photoSelectOptions);
    let uris = photoSelectResult.photoUris;
    for (let i = 0; i < uris.length; i++) {
     _ // 获取uri对应的PhotoAsset资产。_
      let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
      predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uris\[i\]);
      let fetchOption: photoAccessHelper.FetchOptions = {
        fetchColumns: \[\],
        predicates: predicates,
      };
      let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> =
        await phAccessHelper.getAssets(fetchOption);
      let photoAsset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();
     _ // 获取PhotoAsset对应的动态照片对象。_
      await photoAccessHelper.MediaAssetManager.requestMovingPhoto(context, photoAsset, {
        deliveryMode: photoAccessHelper.DeliveryMode.FAST\_MODE,
      }, {
        async onDataPrepared(movingPhoto: photoAccessHelper.MovingPhoto) {
        _  // 保存动态图片图片部分的沙箱文件地址_
          let imageFileUri = 'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image.jpg';
        _  // 保存动态图片视频部分的沙箱文件地址_
          let videoFileUri = 'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video.mp4';
          let imageFile: fileIo.File | undefined;
          let videoFile: fileIo.File | undefined;
          if (movingPhoto !== undefined) {
            console.info(\`request moving photo successfully, uri: ${movingPhoto.getUri()}\`);
            try {
              fileIo.openSync(imageFileUri, fileIo.OpenMode.CREATE);
              fileIo.openSync(videoFileUri, fileIo.OpenMode.CREATE);
              await movingPhoto.requestContent(imageFileUri, videoFileUri);
            } catch (err) {
              console.error(\`failed to moving photo\`);
            } finally {
              if (imageFile) {
                fileIo.closeSync(imageFile);
              }
              if (videoFile) {
                fileIo.closeSync(videoFile);
              }
            }
          }
        },
      });
    }
  } catch (err) {
    console.error(\`request moving photo failed with error: ${err.code}, ${err.message}\`);
  }
}


@Entry
@Component
struct Index {
  @State src: photoAccessHelper.MovingPhoto | undefined = undefined;
  controller: MovingPhotoViewController = new MovingPhotoViewController();
  avTranscoder: media.AVTranscoder | undefined = undefined;


  async aboutToAppear(): Promise<void> {
    try {
      this.avTranscoder = await media.createAVTranscoder();
    } catch (err) {
      console.error(\`failed to create AVTranscoder: ${JSON.stringify(err)}\`);
      return;
    }
    this.avTranscoder.on('complete', async () => {
      console.info(\`AVTranscoder completed\`);
    });
    this.avTranscoder.on('error', (err: BusinessError) => {
      console.error(\`AVTranscoder failed: ${JSON.stringify(err)}\`);
    });
    this.avTranscoder.on('progressUpdate', (progress: number) => {
      console.info(\`AVTrancoder progress update: ${progress}\`);
    });
  }


  build() {
    Column({ space: 20 }) {
      Column() {
        MovingPhotoView({
          movingPhoto: this.src,
          controller: this.controller,
        });
      }
      .width('50%')
      .aspectRatio(1);


      Row() {
        Button('Start Play')
          .margin(5)
          .onClick(() => {
            this.controller.startPlayback();
          });
        Button('Stop Play')
          .margin(5)
          .onClick(() => {
            this.controller.stopPlayback();
          });
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .height('10%');


      Button('Pick Moving Photo').onClick(async () => {
        let context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
        let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
        pickMovingPhoto(phAccessHelper, context);
      }).width('100%');


      Button('Pack Image')
        .width('100%')
        .onClick(async () => {
         _ // 保存动态照片图片内容的沙箱文件地址_
          let imageFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image.jpg';
        _  // 保存压缩后的图片文件的沙箱文件地址_
          let packFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
          let imageFile: fileIo.File | undefined;
          let packFile: fileIo.File | undefined;
          try {
            imageFile = fileIo.openSync(imageFileUri, fileIo.OpenMode.READ\_WRITE);
            packFile = fileIo.openSync(packFileUri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ\_WRITE);
            let imageSource = image.createImageSource(imageFile.fd);
            let imagePacker = image.createImagePacker();
            let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 50 };
            await imagePacker.packToFile(imageSource, packFile.fd, packOpts);
          } catch (err) {
            console.error(\`failed to pack image file: ${JSON.stringify(err)}\`);
          } finally {
            if (imageFile) {
              fileIo.closeSync(imageFile);
            }
            if (packFileUri) {
              fileIo.closeSync(packFile);
            }
          }
        });


      Button('Pack Video')
        .width('100%')
        .onClick(async () => {
        _  // 保存动态照片视频内容的沙箱文件地址_
          let videoFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video.mp4';
       _   // 保存压缩后的视频文件的沙箱文件地址_
          let packFileUri =
            'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
          let videoFile: fileIo.File | undefined;
          let packFile: fileIo.File | undefined;
          try {
            videoFile = fileIo.openSync(videoFileUri, fileIo.OpenMode.READ\_WRITE);
            packFile = fileIo.openSync(packFileUri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ\_WRITE);
            let videoFileSize = fileIo.statSync(videoFile.fd).size;
            this.avTranscoder!.fdSrc = { fd: videoFile.fd, offset: 0, length: videoFileSize };
            this.avTranscoder!.fdDst = packFile.fd;
            let config: media.AVTranscoderConfig = {
              audioCodec: media.CodecMimeType.AUDIO\_AAC,
              videoCodec: media.CodecMimeType.VIDEO\_HEVC,
              fileFormat: media.ContainerFormatType.CFT\_MPEG\_4,
              audioBitrate: 20000,
              videoBitrate: 10000000,
            };
            await this.avTranscoder!.prepare(config);
            await this.avTranscoder!.start();
          } catch (err) {
            console.error(\`failed to pack video file: ${JSON.stringify(err)}\`);
          } finally {
            if (videoFile) {
              fileIo.closeSync(videoFile);
            }
            if (packFile) {
              fileIo.closeSync(packFile);
            }
          }
        });


      Button('Load Moving Photo')
        .width('100%')
        .onClick(async () => {
          let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
          try {
          _  // 压缩后的图片文件的沙箱地址_
            let imageFileUri =
              'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
          _  // 压缩后的视频文件的沙箱地址_
            let videoFileUri =
              'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
            this.src =
              await photoAccessHelper.MediaAssetManager.loadMovingPhoto(context, imageFileUri, videoFileUri);
            console.info(\`Load moving photo successfully\`);
          } catch (err) {
            console.error(\`Load Moving photo failed: ${JSON.stringify(err)}\`);
          }
        });


      SaveButton({
        icon: SaveIconStyle.FULL\_FILLED,
        text: SaveDescription.SAVE\_IMAGE,
        buttonType: ButtonType.Capsule,
      }) _// 创建安全控件按钮。_
        .onClick(async (\_event, result: SaveButtonOnClickResult) => {
          if (result === SaveButtonOnClickResult.SUCCESS) {
            try {
              let context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
              let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
             _ // 压缩后的图片文件的沙箱地址_
              let imageFileUri =
                'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_image\_packed.jpg';
            _  // 压缩后的视频文件的沙箱地址_
              let videoFileUri =
                'file://com.huawei.myapplication/data/storage/el2/base/haps/entry/files/moving\_video\_packed.mp4';
              let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
                photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context,
                  photoAccessHelper.PhotoType.IMAGE, 'jpg', {
                    title: 'moving\_photo',
                    subtype: photoAccessHelper.PhotoSubtype.MOVING\_PHOTO,
                  });
              assetChangeRequest.addResource(photoAccessHelper.ResourceType.IMAGE\_RESOURCE, imageFileUri);
              assetChangeRequest.addResource(photoAccessHelper.ResourceType.VIDEO\_RESOURCE, videoFileUri);
              await phAccessHelper.applyChanges(assetChangeRequest);
              console.info(\`create moving photo successfully, uri: ${assetChangeRequest.getAsset().uri}\`);
            } catch (err) {
              console.error(\`create moving photo failed with error: ${err.code}, ${err.message}\`);
            }
          } else {
            console.error('SaveButtonOnClickResult create moving photo failed');
          }
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}