鸿蒙6.0应用开发——图片合成视频

概述

在个人相册制作、电商产品展示、理财销售回放等多个场景中,都需要将图片合成视频。开发者通过调用Image Kit、视频编码、媒体数据封装提供的接口,可以实现图片合成视频的功能。

  • Image Kit提供图片的解码、编码、编辑、元数据处理等功能。
  • 视频编码可将未压缩的视频数据压缩为视频码流,如H.264、H.265。
  • 媒体数据封装可完成媒体文件的封装,将编码后的音视频数据,按一定的格式写入媒体文件中。

本文以图库图片合成视频场景为例,介绍图片解码、图片数据编码、视频生成的主要步骤,并给出开发过程中常见问题的分析思路和解决方案。

图库图片合成视频

场景描述

以将图库中的图片转换为MP4文件为例,本场景展示使用图片和编解码的基础能力来实现图片合成视频的功能。

在这里插入图片描述

实现原理

Image Kit中的PixelMap是用于读取或写入图像数据以及获取图像信息的图像像素类。在图片合成视频的过程中,首先将图片解码为PixelMap,然后使用Buffer模式编码,将PixelMap中保存的图像数据复制到编码器的输入buffer中,未压缩的YUV输出成已压缩的视频码流H.264,编码完成后封装成视频文件。

在这里插入图片描述

如果需要使用Surface模式编码,将图片解码成PixelMap后,需要先从编码器的NativeWindow申请buffer,然后将PixelMap中保存的图像数据复制到申请的buffer中并提交buffer,编码完成后再封装为视频文件,具体实现可以参考NativeWindow开发指导 (C/C++)。

开发步骤

  1. 使用PhotoViewPicker从图库中选择图片,数量不少于两张。当选择数量不足两张时,将弹出提示框。

    // Use photoAccessHelper to pull up the gallery and select images.
    let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    let photoPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select(photoSelectOptions)
      .then(async (PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
        this.imageUri = PhotoSelectResult.photoUris;
        if (this.imageUri.length < 2) {
          this.showToast($r('app.string.Please_select_at_least_two_images'), 2000);
          return;
        } else {
          this.dialogController.open();
          await this.processImages();
          this.synthesis();
        }
      })
      .catch((err: BusinessError) => {
        hilog.error(0x0000, TAG, `PhotoViewPicker.select failed, error: ${err.code}, ${err.message}`);
      });
    

    代码逻辑走读:

    1. 创建一个photoSelectOptions对象,并设置其MIMEType为图片类型。
    2. 创建一个photoPicker对象,用于选择图片。
    3. 调用photoPicker.select方法,传入photoSelectOptions,开始选择图片。
    4. 使用then方法处理选择成功的结果:
      • 将选择的图片URI存储在this.imageUri中。
      • 检查this.imageUri的长度是否小于2,如果是,则调用this.showToast方法显示提示信息,并返回。
      • 如果this.imageUri的长度不小于2,则打开对话框this.dialogController.open(),并等待图片处理完成。
      • 调用this.processImages()方法处理图片。
      • 调用this.synthesis()方法合成图片。
    5. 使用catch方法处理选择失败的错误,并记录错误信息。
  2. 遍历处理从图库选择的图片。

    (1)读取图片数据并创建ImageSource。

    (2)配置图片解码参数,使用imageSource.createPixelMap()获取解码后的PixelMap。

    (3)将PixelMap保存到队列中。

    // Decode the image and pass it to the native.
    async processImages() {
      for (let i = 0; i < this.imageUri.length; i++) {
        // Read image file data.
        let imgData: ArrayBuffer | undefined;
        let imgFile: fileIo.File | undefined;
        try {
          imgFile = fileIo.openSync(this.imageUri[i], fileIo.OpenMode.READ_ONLY);
          let stat: fileIo.Stat = fileIo.statSync(imgFile.fd);
          imgData = new ArrayBuffer(stat.size);
          fileIo.readSync(imgFile.fd, imgData);
        } catch (err) {
          hilog.error(0x0000, 'testTag', `failed to open uri. code=${err.code},message=${err.message}`);
        } finally {
          if (imgFile) {
            try {
              fileIo.closeSync(imgFile);
            } catch (err) {
              hilog.error(0x0000, 'testTag', `failed to close fileIo. code=${err.code},message=${err.message}`);
            }
          }
        }
        // Decoding images.
        let imageSource: image.ImageSource | undefined;
        let pixelMap: image.PixelMap | undefined;
        try {
          imageSource = image.createImageSource(imgData);
          // Set the decoding bitmap size to be consistent with the first image.
          if (this.imageWidth === 0 && this.imageHeight === 0) {
            let imageInfo: image.ImageInfo = imageSource.getImageInfoSync();
            this.imageWidth = imageInfo.size.width;
            this.imageHeight = imageInfo.size.height;
          }
          let decodingOptions: image.DecodingOptions = {
            editable: true,
            desiredPixelFormat: image.PixelMapFormat.NV12,
            desiredSize: { width: this.imageWidth, height: this.imageHeight }
          }
          pixelMap = await imageSource.createPixelMap(decodingOptions);
          transcoding.pushPixelMap(pixelMap);
        } catch (err) {
          hilog.error(0x0000, 'testTag', `failed to add pictures. code=${err.code},message=${err.message}`);
        } finally {
          if (imageSource) {
            imageSource.release();
          }
          if (pixelMap) {
            pixelMap.release();
          }
        }
      }
    }
    

    代码逻辑走读:

    1. 循环处理图像URI:代码首先遍历this.imageUri数组,每个元素代表一个图像文件的URI。
    2. 读取图像文件数据:
      • 使用fileIo.openSync以只读模式打开图像文件。
      • 使用fileIo.statSync获取文件状态,创建与文件大小相同的ArrayBuffer
      • 使用fileIo.readSync将文件数据读取到ArrayBuffer中。
    3. 错误处理与文件关闭:
      • 如果在读取文件时发生错误,使用hilog.error记录错误信息。
      • 无论是否发生错误,最终都会尝试关闭文件。
    4. 图像解码:
      • 使用image.createImageSourceArrayBuffer创建ImageSource
      • 如果尚未设置图像宽度和高度,使用getImageInfoSync获取图像尺寸。
      • 使用createPixelMap解码图像,设置解码选项以确保图像的像素格式和尺寸符合要求。
    5. 图像处理完成后的清理:
      • 使用transcoding.pushPixelMap将解码后的图像传递给本地环境。
      • 无论解码过程中是否发生错误,最终都会释放ImageSourcePixelMap资源。
  3. 创建编码器和封装器。

    (1)初始化视频编码环境。

    // Initialize the encoder environment, create and configure the muxer.
    int32_t Transcoding::InitEncoder() {
        CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started.");
        CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr && videoEncoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR,
                                 "Already started.");
    
        videoEncoder_ = std::make_unique<VideoEncoder>();
        muxer_ = std::make_unique<Muxer>();
    
        int32_t ret = muxer_->Create(sampleInfo_.outputFd);
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create muxer with fd(%{public}d) failed",
                                 sampleInfo_.outputFd);
        ret = muxer_->Config(sampleInfo_);
    
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder failed");
    
        ret = CreateVideoEncoder();
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed");
    
        AVCODEC_SAMPLE_LOGI("Succeed");
        return AVCODEC_SAMPLE_ERR_OK;
    }
    

    (2)创建封装器。

    int32_t Muxer::Create(int32_t fd) {
        muxer_ = OH_AVMuxer_Create(fd, AV_OUTPUT_FORMAT_MPEG_4);
        CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer create failed, fd: %{public}d", fd);
        return AVCODEC_SAMPLE_ERR_OK;
    }
    

    (3)创建编码器。

    // Create and configure encoder.
    int32_t Transcoding::CreateVideoEncoder() {
        int32_t ret = videoEncoder_->Create(sampleInfo_.outputVideoCodecMime);
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed");
    
        videoEncContext_ = new CodecUserData;
        ret = videoEncoder_->Config(sampleInfo_, videoEncContext_);
        CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder config failed");
    
        return AVCODEC_SAMPLE_ERR_OK;
    }
    
  4. 对图片进行编码和封装。

    (1)启动编码器和封装器,启动编码输入输出处理线程。

    // Start the encoder, create input and output thread.
    int32_t Transcoding::Start() {
        std::unique_lock<std::mutex> lock(mutex_);
        int32_t ret;
        CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started.");
        if (videoEncContext_) {
            CHECK_AND_RETURN_RET_LOG(videoEncoder_ != nullptr && muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR,
                                     "Already started.");
            int32_t ret = muxer_->Start();
            CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Muxer start failed");
            ret = videoEncoder_->Start();
            isStarted_ = true;
            CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder start failed");
            videoEncInputThread_ = std::make_unique<std::thread>(&Transcoding::VideoEncInputThread, this);
            videoEncOutputThread_ = std::make_unique<std::thread>(&Transcoding::VideoEncOutputThread, this);
            if (videoEncInputThread_ == nullptr || videoEncOutputThread_ == nullptr) {
                AVCODEC_SAMPLE_LOGE("Create thread failed");
                StartRelease();
                return AVCODEC_SAMPLE_ERR_ERROR;
            }
        }
        if (isReleased_) {
            isReleased_ = false;
            videoEncContext_->outputFrameCount = 0;
        }
        AVCODEC_SAMPLE_LOGI("Succeed");
        doneCond_.notify_all();
        return AVCODEC_SAMPLE_ERR_OK;
    }
    

    (2)编码器输入线程从PixelMap队列中取出待编码数据,然后复制到编码器输入队列中。

    // Encoding input thread.
    void Transcoding::VideoEncInputThread() {
        while (true) {
            OH_LOG_ERROR(LOG_APP, "do VideoEncInputThread while");
            std::unique_lock<std::mutex> lock(videoEncContext_->outputMutex);
            bool condRet = videoEncContext_->inputCond.wait_for(
                lock, 5s, [this]() { return !isStarted_ || !videoEncContext_->inputBufferInfoQueue.empty(); });
            CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
            CHECK_AND_CONTINUE_LOG(!videoEncContext_->inputBufferInfoQueue.empty(),
                                   "Buffer queue is empty, continue, cond ret: %{public}d", condRet);
            // get Buffer from inputBufferInfoQueue.
            CodecBufferInfo bufferInfo = videoEncContext_->inputBufferInfoQueue.front();
            videoEncContext_->inputBufferInfoQueue.pop();
            videoEncContext_->inputFrameCount++;
            lock.unlock();
            if (!pictures.empty()) {
                // Get the data of the current frame.
                OH_PixelmapNative *currentFrame = pictures.front();
                pictures.pop();
                CopyStrideYUV420SP(bufferInfo, currentFrame);
            } else {
                bufferInfo.attr.size = 0;
                bufferInfo.attr.offset = 0;
                bufferInfo.attr.pts = 0;
                bufferInfo.attr.flags = AVCODEC_BUFFER_FLAGS_EOS;
            }
            int32_t ret = videoEncoder_->PushInputBuffer(bufferInfo);
            CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Push data failed, thread out");
            AVCODEC_SAMPLE_LOGW(
                "Out bufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}" PRId64,
                bufferInfo.attr.flags, bufferInfo.attr.offset, bufferInfo.attr.pts, bufferInfo.attr.size);
            if (bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS) {
                AVCODEC_SAMPLE_LOGW("VideoDecOutputThread Catch EOS, thread out" PRId64);
                break;
            }
        }
    }
    

    (3)编码器输出线程从编码器输出队列中取出已编码的数据送入封装器。

    // Encoding output thread.
    void Transcoding::VideoEncOutputThread() {
        while (true) {
            std::unique_lock<std::mutex> lock(videoEncContext_->outputMutex);
            bool condRet = videoEncContext_->outputCond.wait_for(
                lock, 5s, [this]() { return !isStarted_ || !videoEncContext_->outputBufferInfoQueue.empty(); });
            CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out");
            CHECK_AND_CONTINUE_LOG(!videoEncContext_->outputBufferInfoQueue.empty(),
                                   "Buffer queue is empty, continue, cond ret: %{public}d", condRet);
    
            CodecBufferInfo bufferInfo = videoEncContext_->outputBufferInfoQueue.front();
            videoEncContext_->outputBufferInfoQueue.pop();
            CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS),
                                "VideoEncOutputThread  Catch EOS, thread out");
            lock.unlock();
            if ((bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_SYNC_FRAME) ||
                (bufferInfo.attr.flags == AVCODEC_BUFFER_FLAGS_NONE)) {
                videoEncContext_->outputFrameCount++;
                bufferInfo.attr.pts = videoEncContext_->outputFrameCount * MICROSECOND / sampleInfo_.outputFrameRate;
            } else {
                bufferInfo.attr.pts = 0;
            }
            AVCODEC_SAMPLE_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}" PRId64,
                                videoEncContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags,
                                bufferInfo.attr.pts);
            muxer_->WriteSample(muxer_->GetVideoTrackId(), reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer),
                                bufferInfo.attr);
            int32_t ret = videoEncoder_->FreeOutputBuffer(bufferInfo.bufferIndex);
            CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Encoder output thread out");
            CHECK_AND_BREAK_LOG(videoEncContext_->outputFrameCount != sampleInfo_.imgCount, "Encoder output thread out");
        }
        AVCODEC_SAMPLE_LOGI("Exit, frame count: %{public}u", videoEncContext_->outputFrameCount);
        StartRelease();
    }
    
  5. 所有图片都处理完成后,使用Video组件播放合成后的视频。

    // Play videos using the Video component.
    Video({
      src: this.videoSrc,
      controller: this.controller
    })
      .width('100%')
      .height('100%')
      .autoPlay(true)
      .controls(false)
      .objectFit(1)
      .zIndex(0)
      .onPrepared((event) => {
        if (event) {
          this.durationTime = event.duration;
        }
      })
      .onUpdate((event) => {
        if (event) {
          this.currentTime = event.time;
        }
      })
      .onFinish(() => {
        this.isStart = !this.isStart;
      })
      .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp }))
    
  6. 使用SaveButton安全控件,将视频保存到图库。

    (1)创建安全控件按钮。

    // Use SaveButton Component to Save Video.
    SaveButton({ text: SaveDescription.SAVE_TO_GALLERY })
      .width('100%')
      .height(40)
      .backgroundColor(this.showVideo ? 'rgba(10,89,247)' : 'rgba(10,89,247,0.4)')
      .onClick(async (event, result: SaveButtonOnClickResult) => {
        if (!this.showVideo) {
          return;
        }
        if (result === SaveButtonOnClickResult.SUCCESS) {
          try {
            this.saveVideo();
            this.showToast($r('app.string.Save_Success'));
          } catch (err) {
            hilog.error(0x0000, TAG, 'createAsset failed, error: ' + JSON.stringify(err));
          }
        } else {
          hilog.error(0x0000, TAG, 'SaveButtonOnClickResult create asset failed.');
        }
      })
    

    (2)调用MediaAssetChangeRequest.createImageAssetRequest()和PhotoAccessHelper.applyChanges()接口,将视频保存到图库。

    // Save videos using photoAccessHelper.
    saveVideo() {
      let context = this.getUIContext().getHostContext();
      let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
      let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest | undefined;
      try {
        assetChangeRequest =
          photoAccessHelper.MediaAssetChangeRequest.createVideoAssetRequest(context, this.videoSrc);
      } catch (error) {
        let err = error as BusinessError;
        hilog.error(0x0000, 'openSync', `openSync failed. code =${err.code}, message =${err.message}`);
      }
      phAccessHelper.applyChanges(assetChangeRequest).then(() => {
        hilog.info(0x0000, 'testTag', '%{public}s', 'apply Changes successfully');
      }).catch((err: BusinessError) => {
        hilog.error(0x0000, 'testTag', `apply Changes failed. code=${err.code},message=${err.message}`);
      });
    }
    
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐