• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import image from '@ohos.multimedia.image';
17import fileUri from '@ohos.file.fileuri';
18import fs from '@ohos.file.fs';
19import router from '@ohos.router';
20import { common } from '@kit.AbilityKit';
21import { promptAction } from '@kit.ArkUI';
22// mp4parser 三方库组件
23import { IFrameCallBack, ICallBack, MP4Parser } from '@ohos/mp4parser';
24import CommonConstants from '../constants/Constants';
25
26import { CustomSetServerDlg } from '../customcomponents/CustomSetServerDlg';
27import { CustomLoadingProgressDlg } from '../customcomponents/CustomLoadingProgressDlg';
28import { TimeUtils } from '../utils/TimeUtils';
29import { logger } from '../utils/Logger';
30import { getTimeStr, savePixelMap } from '../utils/FileUtil';
31import { requestUpload } from '../uploadanddownload/RequestUpload';
32import { urlUtils } from '../utils/UrlUtils';
33import { VideoPlayListener } from '../videoplaycomponents/VideoPlayListener';
34import { VideoTrimmerOption } from '../videotrimmer/VideoTrimmerOption';
35import { VideoTrimListener } from '../videotrimmer/VideoTrimListener';
36import { VideoLoadFramesListener } from '../videotrimmer/VideoLoadFramesListener';
37import { VideoTrimmerView } from '../videotrimmer/VideoTrimmerView';
38import { WorkItem } from '../model/WorkItemModel';
39
40const TAG: string = 'videoTrimmer_upload';
41
42@Entry
43@Component
44export struct VideoUpload {
45  @State centerIndex: number = 0; // List显示区域内中间子组件索引值
46  @State isTrimmer: boolean = false; // 是否处于剪辑状态
47  private uploadTimesMax: number = 5; // 上传重试次数
48  @State serverUrl: string = ''; // 服务器地址
49  @State workItem: WorkItem = new WorkItem('1月3日', '15:40', '', '', '北高峰', true);
50  @State mVideoPlayListener: VideoPlayListener = {
51    onPrepared: () => {
52    },
53    onComplete() {
54    },
55    onPlayStatus: (isPlay: boolean) => {
56    },
57    onTimeUpdate(time: number) {
58      this.currentTime = time;
59    },
60    onBitrateUpdate(bitrateList: number[]) {
61    },
62    onErrorUpdate(error: string) {
63    },
64  }
65  @State mVideoTrimmerOption: VideoTrimmerOption = new VideoTrimmerOption();
66
67  /**
68   * 获取视频第一张图片
69   */
70  async getVideoFirstImage(videoSrc: string): Promise<void> {
71    let callBack: ICallBack = {
72      //  回调函数
73      callBackResult: (code: number) => {
74        if (code === 0) {
75          let frameCallBack: IFrameCallBack = {
76            callBackResult: async (data: ArrayBuffer, timeUs: number) => {
77              const imageSource: image.ImageSource = image.createImageSource(data);
78
79              let decodingOptions: image.DecodingOptions = {
80                sampleSize: 1,
81                editable: true,
82                desiredSize: { width: CommonConstants.firstImageWidth, height: CommonConstants.firstImageHeight },
83                desiredPixelFormat: image.PixelMapFormat.RGBA_8888
84              };
85              await imageSource.createPixelMap(decodingOptions).then(async (px: image.PixelMap) => {
86                this.workItem.firstImage = px;
87                imageSource.release();
88              })
89            }
90          }
91          let startTimeUs = CommonConstants.FIRST_IMAGE_START_TIME + '';
92          let endTimeUs = CommonConstants.FIRST_IMAGE_END_TIME + '';
93          // TODO: 知识点:传入起始时间,通过MP4Parser获取视频的首页图片
94          MP4Parser.getFrameAtTimeRang(startTimeUs, endTimeUs, MP4Parser.OPTION_CLOSEST, frameCallBack);
95        }
96      }
97    }
98    // TODO: 知识点:设置MP4Parser视频源地址及回调函数
99    MP4Parser.setDataSource(videoSrc, callBack);
100  }
101
102  // 配置服务器地址
103  customSetServerDlg: CustomDialogController = new CustomDialogController({
104    builder: CustomSetServerDlg({
105      serverUrl: getContext(this)
106        .resourceManager
107        .getStringSync($r('app.string.video_trimmer_default_serverIP'))
108    }),
109    alignment: DialogAlignment.Center, // 自定义弹窗底端对齐
110    autoCancel: true,
111    cancel: this.onCancel, // 返回、ESC键和点击遮障层弹窗退出时回调
112    customStyle: true, // 弹窗样式自定义
113  })
114
115  // 返回、ESC键和点击遮障层弹窗退出时回调
116  onCancel(): void {
117    this.customSetServerDlg.close();
118  }
119
120  // 校验是否配置服务器
121  async checkServerAddress(): Promise<boolean> {
122    // 获取服务器地址
123    this.serverUrl = await urlUtils.getUrl(getContext(this) as common.UIAbilityContext);
124    if (this.serverUrl === undefined || this.serverUrl === '') {
125      await this.customSetServerDlg.open();
126    }
127    this.serverUrl = await urlUtils.getUrl(getContext(this) as common.UIAbilityContext);
128    logger.info(TAG, 'serverUrl is = ' + this.serverUrl);
129    if (this.serverUrl !== undefined && this.serverUrl.length > 1) {
130      return true;
131    } else {
132      return false;
133    }
134  }
135
136  //  保存首页背景图
137  async saveFirstImage() {
138    const url = await savePixelMap(getContext(this), this.workItem.firstImage as PixelMap, getTimeStr());
139    this.workItem.firstImage = fileUri.getUriFromPath(url);
140    this.workItem.date = TimeUtils.getCurrentTime();
141    AppStorage.setOrCreate('addWorkItem', this.workItem);
142    await this.customSetServerDlg.close();
143    router.back();
144  }
145
146  // TODO: 知识点: 保存视频到服务器
147  async uploadFiles(outVideoPath: string) {
148    // 校验是否配置服务器
149    if (!await this.checkServerAddress()) {
150      logger.error(TAG, 'serverUrl is null. ');
151      return;
152    }
153
154    let countdown: number = 0; // 重试计数
155    let imageList: string[] = [];
156    imageList.push(outVideoPath);
157    await requestUpload.uploadFiles(imageList, (progress: number, isSucceed: boolean) => {
158      progress = progress;
159      if (progress === CommonConstants.PROGRESS_MAX && isSucceed) {
160        logger.info(TAG, 'RequestUpload success. ');
161        // 上传成功后删除裁剪的视频
162        if (this.workItem.trimmerSrc !== '') {
163          fs.unlinkSync(this.workItem.trimmerSrc);
164        }
165        // 视频地址替换为服务器上的视频
166        this.workItem.videoSrc = this.serverUrl + this.workItem.videoSrc.split('/').pop();
167        // 保存视频首页背景图
168        this.saveFirstImage();
169      }
170      if (progress === CommonConstants.PROGRESS_MAX && isSucceed === false) {
171        countdown = this.uploadTimesMax;
172        let interval = setInterval(() => {
173          if (countdown > 0) {
174            countdown--;
175          } else {
176            clearInterval(interval);
177          }
178        }, CommonConstants.UPLOAD_INTERVAL_TIME);
179        promptAction.showToast({ message: $r('app.string.video_trimmer_upload_fail') })
180      }
181    });
182  }
183
184  async aboutToAppear(): Promise<void> {
185    const params = router.getParams() as Record<string, string>; // 获取传递过来的参数对象
186    if (params) {
187      this.workItem.videoSrc = params.value as string; // 获取参数value的值
188      this.workItem.trimmerSrc = '';
189      logger.info(TAG, 'the source video path is:' + this.workItem.videoSrc);
190      // 获取视频第一张图片
191      await this.getVideoFirstImage(this.workItem.videoSrc);
192      // 视频剪辑参数选项
193      let tempOption = new VideoTrimmerOption();
194      tempOption.listener = this.initListener;
195      tempOption.loadFrameListener = this.initLoadFrameListener;
196      tempOption.srcFilePath = this.workItem.videoSrc;
197      this.mVideoTrimmerOption = tempOption;
198    }
199  }
200
201  async aboutToDisappear(): Promise<void> {
202    fs.unlinkSync(this.mVideoTrimmerOption.srcFilePath);
203    this.customSetServerDlg.close();
204  }
205
206  // 加载进度对话框
207  dialogController: CustomDialogController = new CustomDialogController({
208    builder: CustomLoadingProgressDlg({}),
209    autoCancel: false,
210    alignment: DialogAlignment.Center,
211    offset: { dx: 0, dy: 0 },
212    customStyle: false
213  })
214  // 视频剪辑回调接口接口
215  initListener: VideoTrimListener = {
216    onStartTrim: () => {
217      logger.info(TAG, '开始裁剪');
218      this.dialogController.open();
219    },
220    onFinishTrim: async (outVideoPath: string) => {
221      this.dialogController.close();
222      // 更新上传视频地址
223      this.workItem.trimmerSrc = outVideoPath;
224      await this.getVideoFirstImage(this.workItem.trimmerSrc);
225      this.isTrimmer = false;
226      logger.info(TAG, '裁剪成功 path=' + this.workItem.trimmerSrc)
227    },
228    onCancel: () => {
229      logger.info(TAG, '用户取消');
230      this.isTrimmer = false;
231    }
232  }
233  // 视频帧加载回调接口
234  initLoadFrameListener: VideoLoadFramesListener = {
235    onStartLoad: () => {
236      logger.info(TAG, '开始获取帧数据')
237    },
238    onFinishLoad: () => {
239      logger.info(TAG, '获取帧数据结束')
240    }
241  }
242
243  build() {
244    Column() {
245      if (!this.isTrimmer) {
246        this.VideoUpdateInfo();
247      } else {
248        // TODO: 知识点: 视频剪辑组件VideoTrimmerView。 videoTrimmerOption:视频剪辑相关参数
249        VideoTrimmerView({ videoTrimmerOption: this.mVideoTrimmerOption!! })
250      }
251    }
252    .backgroundColor(Color.Black)
253    .width($r('app.string.video_trimmer_full_size'))
254    .height($r('app.string.video_trimmer_full_size'))
255  }
256
257  /**
258   * 视频上传信息
259   */
260  @Builder
261  VideoUpdateInfo() {
262    Row() {
263      // 左边取消按钮
264      Text($r('app.string.video_trimmer_cancel'))
265        .height($r('app.integer.video_trimmer_upload_info_height'))
266        .fontColor(Color.White)
267        .margin({
268          left: $r('app.integer.video_trimmer_component_video_playing_image_margin_left')
269        })
270        .id('upload_cancel')
271        .alignSelf(ItemAlign.Center)
272        .onClick(() => {
273          promptAction.showDialog({
274            alignment: DialogAlignment.Center,
275            isModal: false,
276            message: $r('app.string.video_trimmer_cancel_confirm'),
277            buttons: [
278              {
279                text: $r('app.string.video_trimmer_cancel'),
280                color: $r('app.color.video_trimmer_cancel')
281              },
282              {
283                text: $r('app.string.video_trimmer_ok'),
284                color: $r('app.color.video_trimmer_confirm')
285              }
286            ]
287          }, async (err, data) => {
288            if (err) {
289              logger.error(TAG, 'showDialog err: ' + err);
290              return;
291            }
292            if (data.index === 1) {
293              // 确认上传视频
294              router.back();
295            }
296          });
297        })
298      Blank()
299      // 右边保存按钮
300      Text($r('app.string.video_trimmer_save'))
301        .id('upload_save')
302        .width($r('app.integer.video_trimmer_update_save_width'))
303        .height($r('app.integer.video_trimmer_upload_info_height'))
304        .fontColor(Color.White)
305        .backgroundColor($r('app.color.video_trimmer_save_background'))
306        .margin({
307          right: $r('app.integer.video_trimmer_component_video_playing_image_margin_left')
308        })
309        .textAlign(TextAlign.Center)
310        .borderRadius($r('app.integer.video_trimmer_share_borderRadius'))
311        .onClick(async () => {
312          this.workItem.date = TimeUtils.getCurrentTime();
313          // 剪辑成功后上传剪辑的视频,否则上传原视频
314          if (this.workItem.trimmerSrc != '') {
315            this.uploadFiles(this.workItem.trimmerSrc);
316          } else {
317            this.uploadFiles(this.workItem.videoSrc);
318          }
319        })
320    }
321    .alignItems(VerticalAlign.Center)
322    .height($r('app.integer.video_trimmer_upload_top_height'))
323    .width($r('app.string.video_trimmer_full_size'))
324
325    // 背景图
326    Stack({ alignContent: Alignment.Center }) {
327      Image(this.workItem.firstImage)
328        .id('image_to_trimmer')
329        .height($r('app.integer.video_trimmer_upload_video_height'))
330        .onClick(() => {
331          if (this.workItem.trimmerSrc !== '') {
332            fs.unlinkSync(this.workItem.trimmerSrc);
333            this.workItem.trimmerSrc = '';
334          }
335          this.isTrimmer = true;
336        })
337      // 播放按钮
338      Image($r('app.media.video_trimmer_icon_pause'))
339        .width($r('app.integer.video_trimmer_component_video_playing_image_width'))
340        .height($r('app.integer.video_trimmer_component_video_playing_image_width'))
341        .onClick(() => {
342          this.isTrimmer = true;
343        })
344    }
345    .margin({
346      left: $r('app.integer.video_trimmer_upload_video_left_margin'),
347      right: $r('app.integer.video_trimmer_upload_video_left_margin'),
348      top: $r('app.integer.video_trimmer_upload_video_top_margin')
349    })
350
351    Row() {
352      Text($r('app.string.video_trimmer_labels'))
353        .id('txtLabels')
354        .width($r('app.integer.video_trimmer_update_button_width'))
355        .textAlign(TextAlign.Center)
356        .fontColor(Color.White)
357        .borderRadius($r('app.integer.video_trimmer_upload_label_radius'))
358        .height($r('app.integer.video_trimmer_upload_info_height'))
359        .backgroundColor($r('app.color.video_trimmer_label_color'))
360      Text($r('app.string.video_trimmer_btn_label'))
361        .width($r('app.integer.video_trimmer_update_button_width'))
362        .textAlign(TextAlign.Center)
363        .fontColor(Color.White)
364        .margin({ left: $r('app.integer.video_trimmer_upload_label_margin') })
365        .height($r('app.integer.video_trimmer_upload_info_height'))
366        .borderRadius($r('app.integer.video_trimmer_upload_label_radius'))
367        .backgroundColor(Color.Gray)
368        .onClick(() => {
369          promptAction.showToast({ message: $r('app.string.video_trimmer_toast_tips') });
370        })
371    }.height($r('app.integer.video_trimmer_upload_label_row_height'))
372    .margin({
373      left: $r('app.integer.video_trimmer_lazy_foreach_list_page_list_item_margin_bottom'),
374      top: $r('app.integer.video_trimmer_upload_label_padding_top'),
375    })
376    .alignSelf(ItemAlign.Start)
377    .align(Alignment.Start)
378
379    Row() {
380      Image($r('app.media.video_trimmer_address'))
381        .backgroundColor(Color.Black)
382        .width($r('app.integer.video_trimmer_update_icon_size'))
383        .height($r('app.integer.video_trimmer_update_icon_size'))
384      Text($r('app.string.video_trimmer_user_address'))
385        .id('txtAddress')
386        .padding({ left: $r('app.integer.video_trimmer_detail_ack_button_left_padding') })
387        .fontColor(Color.White)
388        .textAlign(TextAlign.Center)
389        .onClick(() => {
390          promptAction.showToast({ message: $r('app.string.video_trimmer_toast_tips') });
391        })
392    }
393    .height($r('app.integer.video_trimmer_update_item_height'))
394    .width($r('app.string.video_trimmer_full_size'))
395    .padding({ left: $r('app.integer.video_trimmer_upload_detail_left_padding') })
396
397    Row() {
398      Image($r('app.media.video_trimmer_user_lock'))
399        .width($r('app.integer.video_trimmer_update_icon_size'))
400        .height($r('app.integer.video_trimmer_update_icon_size'))
401        .backgroundColor(Color.Black)
402      Text($r('app.string.video_trimmer_who_can_see'))
403        .padding({ left: $r('app.integer.video_trimmer_detail_ack_button_left_padding') })
404        .textAlign(TextAlign.Center)
405        .fontColor(Color.White)
406      Blank()
407      Text($r('app.string.video_trimmer_all_user'))
408        .id('txtUserCanView')
409        .textAlign(TextAlign.Center)
410        .fontColor(Color.White)
411        .margin({ right: $r('app.integer.video_trimmer_detail_back_margin') })
412        .align(Alignment.End)
413        .onClick(() => {
414          promptAction.showToast({ message: $r('app.string.video_trimmer_toast_tips') });
415        })
416    }
417    .height($r('app.integer.video_trimmer_update_item_height'))
418    .width($r('app.string.video_trimmer_full_size'))
419    .padding({ left: $r('app.integer.video_trimmer_upload_detail_left_padding') })
420
421    Row() {
422      Image($r('app.media.video_trimmer_record_time'))
423        .backgroundColor(Color.Black)
424        .width($r('app.integer.video_trimmer_update_icon_size'))
425        .height($r('app.integer.video_trimmer_update_icon_size'))
426        .alignSelf(ItemAlign.Center)
427        .align(Alignment.Center)
428      Text($r('app.string.video_trimmer_record_time'))
429        .padding({ left: $r('app.integer.video_trimmer_detail_ack_button_left_padding') })
430        .textAlign(TextAlign.Center)
431        .fontColor(Color.White)
432      Blank()
433      Text(TimeUtils.getCurrentTime() + ' >')
434        .id('txtRecordTime')
435        .margin({ right: $r('app.integer.video_trimmer_detail_back_margin') })
436        .textAlign(TextAlign.Center)
437        .fontColor(Color.White)
438        .align(Alignment.End)
439    }
440    .height($r('app.integer.video_trimmer_update_item_height'))
441    .width($r('app.string.video_trimmer_full_size'))
442    .padding({ left: $r('app.integer.video_trimmer_upload_detail_left_padding') })
443    .margin({ bottom: $r('app.integer.video_trimmer_upload_detail_bottom_margin') })
444  }
445}