• Home
Name Date Size #Lines LOC

..--

AppScope/22-Oct-2025-3532

entry/22-Oct-2025-6,5696,164

environment/22-Oct-2025-2113

hvigor/22-Oct-2025-3836

.gitignoreD22-Oct-2025133 1212

README.mdD22-Oct-202532.3 KiB681618

build-profile.json5D22-Oct-20251.3 KiB5857

code-linter.json5D22-Oct-20251.4 KiB4746

hvigorfile.tsD22-Oct-2025839 225

oh-package.json5D22-Oct-2025809 2624

ohosTest.mdD22-Oct-20252.8 KiB129

video_trimmer.gifD22-Oct-20252.9 MiB

README.md

1# 视频下载保存及剪辑压缩上传
2
3### 介绍
4
5本示例主要介绍从网上下载视频到相册,以及从相册中选择视频进行剪辑、压缩、以及上传到服务器进行保存。从相册中选择一个视频保存到沙箱中,再使用FFmpeg命令对沙箱中的视频进行压缩、剪辑。最后使用request.agent将剪辑后的视频上传到服务器进行保存。
6
7### 效果图预览
8
9<img src="./video_trimmer.gif" width="300" >
10
11**使用说明**
12
131. 点击视频列表的背景图片,进入到该视频播放界面。
142. 在视频播放界面,点击右上角的图片按钮,进入到视频分享弹窗。
153. 在视频分享弹窗点击“下载”按钮,将视频下载到相册。
164. 在首页点击右上角的“添加”按钮,从相册中选择要剪辑的视频。
175. 视频选择后,点击界面上视频首页的图片,进入到视频剪辑界面。
186. 在视频剪辑界面,选择要剪辑的时间范围后,点击“完成”按钮,进行视频剪辑。
197. 视频剪辑成功后自动返回到上一页,点击右上角的“保存”按钮,将视频保存到服务器。
20
21### 实现步骤
22
23**下载视频:**
24点击视频列表中某个视频,进入视频播放界面。点击播放界面右上角的图片按钮,进行视频分享界面。点击视频分享界面中的下载按钮,将视频下载到相册保存。
251. 定义下载配置参数 request.agent.Config。源码参考[RequestDownload.ets](./entry/src/main/ets/uploadanddownload/RequestDownload.ets)。
26    ```typescript
27    let downloadConfig: request.agent.Config = {
28      action: request.agent.Action.DOWNLOAD,   // 下载
29      url: url,  // 下载地址
30      method: 'GET',
31      title: 'download',
32      mode: request.agent.Mode.BACKGROUND,  // 下载模式(前台或后台)
33      retry: true,  // 是否支持重试
34      network: request.agent.Network.ANY,  // 支持的网络类型
35      saveas: `./${localPath}`,   // 保存路径
36      overwrite: true             // 是否覆盖已存在文件
37   }
38   ```
392. 通过配置创建下载任务,并监听下载进度。当下载完成后通过callback进行回调通知,并删除下载任务。源码参考[RequestDownload.ets](./entry/src/main/ets/uploadanddownload/RequestDownload.ets)。
40
41    ```typescript
42      try {
43        // 创建下载任务
44        this.downloadTask = await request.agent.create(context, downloadConfig);
45        // 监听下载进度
46        this.downloadTask.on('progress', this.progressCallback = (progress: request.agent.Progress) => {
47          logger.info(TAG, `progress = ${progress.processed} ,state =  ${progress.state}`);
48          let processed = Number(progress.processed.toString()).valueOf();
49          let size = progress.sizes[0];
50          let process: number = Math.floor(processed / size * CommonConstants.PROGRESS_MAX);
51          if (process < CommonConstants.PROGRESS_MAX) {
52            callback(process, false, '');
53          }
54        })
55        // 通过‘completed’监听下载完成
56        this.downloadTask.on('completed', this.completedCallback = (progress: request.agent.Progress) => {
57          logger.info(TAG, `download complete, file= ${url}, progress = ${progress.processed}, localPath=${localPath}`);
58          // 通过回调函数传递下载进度和下载保存的地址
59          callback(CommonConstants.PROGRESS_MAX, true, localPath);
60          // 删除任务
61          this.deleteTask();
62        })
63        // 启动下载任务
64        await this.downloadTask.start();
65      } catch (error) {
66        const err: BusinessError = error as BusinessError;
67        logger.error(TAG, `task  err, Code is ${err.code}, message is ${err.message}`);
68        callback(CommonConstants.PROGRESS_MAX, false, '');
69      }
70    }
71
72    ```
733. 界面收到下载成功的消息后,通过showAssetsCreationDialog拉起权限弹窗,将下载到沙箱中的视频文件通过写数据的方式保存到相册。源码参考[CustomShareDlg.ets](./entry/src/main/ets/customcomponents/CustomShareDlg.ets)。
74
75    ```typescript
76    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
77    try {
78      let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
79        {
80          title: 'videoDoanload',
81          fileNameExtension: 'mp4',
82          photoType: photoAccessHelper.PhotoType.VIDEO,
83          subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
84        }
85      ];
86      // TODO: 知识点: 拉起授予权限的弹窗,获取将视频保存到图库的权限
87      let desFileUris: Array<string> = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
88      logger.info(TAG, 'saveVideo des is:' + desFileUris[0]);
89      // 转换为uri
90      let uri: string = fileUri.getUriFromPath(srcFileUris[0]);
91      // 打开沙箱路径下视频
92      const file: fs.File = fs.openSync(uri, fs.OpenMode.READ_WRITE);
93      // 将沙箱视频内容写入buffer
94      const videoSize: number = fs.statSync(file.fd).size;
95      let arrayBuffer: ArrayBuffer = new ArrayBuffer(videoSize);
96      let readLength: number = fs.readSync(file.fd, arrayBuffer);
97      let videoBuffer: ArrayBuffer = buffer.from(arrayBuffer, 0, readLength).buffer;
98      try {
99        // 打开图库视频保存的路径
100        let fileInAlbum = fs.openSync(desFileUris[0], fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
101        // 写入图库
102        await fs.write(fileInAlbum.fd, videoBuffer);
103        // 关闭文件
104        await fs.close(file.fd);
105        await fs.close(fileInAlbum.fd);
106        logger.info(TAG, 'saveVideo success');
107        // 视频保存成功后,删掉沙箱路径下视频
108        fs.unlinkSync(srcFileUris[0]);
109        promptAction.showToast({
110          message: $r("app.string.video_trimmer_save_success"),
111          duration: DURATION
112        });
113        this.controller.close();
114      } catch (error) {
115        logger.error(TAG, `saveVideo failed, code is: ${error.code}, message is: ${error.message}`);
116      }
117    } catch (err) {
118      logger.error(TAG, `showAssetsCreationDialog failed, errCode is: ${err.code}, message is: ${err.message}`);
119    }
120    ```
121
122**视频剪辑:**
123点击首页右上角的"添加"按钮,从相册中选择一个视频存入沙箱。通过MP4Parser.getFrameAtTimeRang接口,传入起始时间,获取视频的第一张图片进行展示。点击该图片进入视频剪辑界面,视频剪辑完成后自动回到本界面,点击右上角的保存按钮,将剪辑后的视频上传到配置好的服务器。
124
1251. 通过photoViewPicker从相册中选择一个视频。源码参考[VideoTrimmer.ets](./entry/src/main/ets/pages/VideoTrimmer.ets)。
126     ```typescript
127     // 创建图库选项实例
128        const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
129        // 设置选择的媒体文件类型
130        photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
131        // 设置选择媒体文件的最大数目
132        photoSelectOptions.maxSelectNumber = 1;
133        // 创建图库选择器实例
134        const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
135        // 调用photoViewPicker.select()接口拉起图库界面进行图片选择,图片选择成功后,返回photoSelectResult结果集。
136        photoViewPicker.select(photoSelectOptions).then((photoSelectResult) => {
137          if (photoSelectResult !== null && photoSelectResult !== undefined) {
138            // 将视频保存到沙箱中
139            this.saveFileToSandbox(photoSelectResult.photoUris[0]);
140          }
141        }).catch((err: BusinessError) => {
142        logger.error(TAG,
143          `selectPhotoFromAlbum PhotoViewPicker.select failed :, error code: ${err.code}, message: ${err.message}.`);
144        })
145    ```
1462. 将选择的视频写入到沙箱中,写入成功后跳转到视频上传展示界面。源码参考[VideoTrimmer.ets](./entry/src/main/ets/pages/VideoTrimmer.ets)。
147
148      ```typescript
149       /**
150       * 将图库中的视频保存到沙箱中
151       */
152      async saveFileToSandbox(filePathString: string): Promise<void> {
153        this.localSelectVideoUrl[0] = filePathString;
154        try {
155          // 打开图库中视频的文件
156          let resFile = fs.openSync(filePathString, fs.OpenMode.READ_ONLY);
157
158          const dateStr = (new Date().getTime()).toString()
159          let newPath = getContext().cacheDir + "/" + `${dateStr + resFile.name}`;
160          // 将图库中的视频保存到沙箱中
161          fs.copyFileSync(resFile.fd, newPath);
162          // 新的路径
163          this.localSelectVideoUrl[0] = newPath;
164          logger.info(TAG,
165          `selectPhotoFromAlbum, VideoUpload url:${this.localSelectVideoUrl[0].toString()}`);
166          if (this.localSelectVideoUrl[0] !== undefined && this.localSelectVideoUrl[0] !== '') {
167            // 进入到视频上传展示页面
168            DynamicsRouter.pushUri("videotrimmer/VideoUpload", this.localSelectVideoUrl[0]);
169          }
170        } catch (err) {
171          logger.error(TAG, `selectPhotoFromAlbum.select failed :, error code: ${err.code}, message: ${err.message}.`);
172        }
173      }
174      ```
1753. 视频上传展示界面初始时,通过MP4Parser.getFrameAtTimeRang接口获取沙箱中视频的首页图片进行展示。源码参考[VideoUpload.ets](./entry/src/main/ets/pages/VideoUpload.ets)。
176    ```typescript
177    let callBack: ICallBack = {
178      //  回调函数
179      callBackResult: (code: number) => {
180        if (code == 0) {
181          let frameCallBack: IFrameCallBack = {
182            callBackResult: async (data: ArrayBuffer, timeUs: number) => {
183              const imageSource: image.ImageSource = image.createImageSource(data);
184
185              let decodingOptions: image.DecodingOptions = {
186                sampleSize: 1,
187                editable: true,
188                desiredSize: { width: CommonConstants.firstImageWidth, height: CommonConstants.firstImageHeight },
189                desiredPixelFormat: image.PixelMapFormat.RGBA_8888
190              };
191              await imageSource.createPixelMap(decodingOptions).then(async (px: image.PixelMap) => {
192                this.workItem.firstImage = px;
193                imageSource.release();
194              })
195            }
196          }
197          let startTimeUs = CommonConstants.FIRST_IMAGE_START_TIME + '';
198          let endTimeUs = CommonConstants.FIRST_IMAGE_END_TIME + '';
199          // TODO: 知识点:传入起始时间,通过MP4Parser的getFrameAtTimeRang接口获取视频的首页图片
200          MP4Parser.getFrameAtTimeRang(startTimeUs, endTimeUs, MP4Parser.OPTION_CLOSEST, frameCallBack);
201        }
202      }
203    }
204    // TODO: 知识点:设置MP4Parser视频源地址及回调函数
205    MP4Parser.setDataSource(this.workItem.videoSrc, callBack);
206    ```
2074. 视频上传展示界面初始时,配置视频剪辑参数mVideoTrimmerOption。源码参考[VideoUpload.ets](./entry/src/main/ets/pages/VideoUpload.ets)和[视频剪辑参数](./entry/src/main/ets/videotrimmer/VideoTrimmerOption.ets)。
208
209   ```typescript
210   // 视频剪辑参数类
211    export class VideoTrimmerOption {
212    constructor() {
213    this.scaleNum = 100;
214    this.videoMaxTime = 8;
215    this.videoMinTime = 3;
216    this.maxCountRange = 8;
217    this.thumbWidth = 30;
218    this.paddingLineHeight = 10;
219    }
220
221    // 源文件路径
222    @Track srcFilePath: string = '';
223    // 视频剪辑回调接口
224    @Track listener: VideoTrimListener = {
225    onStartTrim() {
226    },
227    onFinishTrim(outputFile: string) {
228    },
229    onCancel() {
230    }
231    };
232    // 视频帧加载回调接口
233    @Track loadFrameListener: VideoLoadFramesListener = {
234    onStartLoad() {
235    },
236    onFinishLoad() {
237    }
238    }
239    // 裁剪事件长度 默认值8秒
240    @Track videoMaxTime?: number = 8;
241    // 最小剪辑时间
242    @Track videoMinTime?: number = 3;
243    // seekBar的区域内一共有多少张图片
244    @Track maxCountRange?: number = 8;
245    // 裁剪视频预览长方形条状左右边缘宽度
246    @Track thumbWidth?: number = 30;
247    // 裁剪视频预览长方形条状上下边缘高度
248    @Track paddingLineHeight?: number = 10;
249    // 当加载帧没有完成,默认的占位图
250    @Track framePlaceholder?: PixelMap;
251    // 裁剪视频预览长方形条状区域背景颜色
252    @Track frameBackground?: string;
253    @Track context?: common.UIAbilityContext;
254    // 裁剪时压缩比率,100 为1:1,即不压缩
255    @Track scaleNum?: number = 100;
256    }
257   ```
258   ```typescript
259   // 视频剪辑参数选项
260   let tempOption = new VideoTrimmerOption();
261   tempOption.listener = this.initListener;
262   tempOption.loadFrameListener = this.initLoadFrameListener;
263   tempOption.srcFilePath = this.workItem.videoSrc;
264   this.mVideoTrimmerOption = tempOption;
265   ```
266
2675. 点击视频图片进入到视频剪辑模块,剪辑成功后自动回到本模块。源码参考[VideoUpload.ets](./entry/src/main/ets/pages/VideoUpload.ets)。
268
269    ```typescript
270    @State isTrimmer: boolean = false;  // 是否处于剪辑状态
271
272    if (!this.isTrimmer) {
273      this.VideoUpdateInfo();
274    } else {
275      // TODO: 知识点: 视频剪辑组件VideoTrimmerView。 videoTrimmerOption:视频剪辑相关参数
276      VideoTrimmerView({ videoTrimmerOption: this.mVideoTrimmerOption!! })
277    }
278   ```
279
2806. 视频剪辑模块初始化时,根据参数中小图的个数,通过MP4Parser.getFrameAtTimeRang命令循环生成小图列表以便用户选择剪辑时间段。其中在生成第一张小图的时候,根据压缩比率参数,生成视频压缩命令。源码参考[VideoTrimmerView.ets](./entry/src/main/ets/videotrimmer/VideoTrimmerView.ets)。。
281
282     ```typescript
283     initImageList() {
284       // 将视频长度分割为一秒一张图片
285       let videoThumbs = new Array<ThumbContent>()
286       for (let i = 0; i < this.mDuration; i = i + CommonConstants.MS_ONE_SECOND) {
287       let temp = new ThumbContent();
288
289         if (this.videoTrimmerOption.framePlaceholder) {
290           temp.framePlaceholder = this.videoTrimmerOption.framePlaceholder;
291         }
292         if (this.videoTrimmerOption.frameBackground) {
293           temp.frameBackground = this.videoTrimmerOption.frameBackground;
294         }
295         videoThumbs.push(temp);
296       }
297
298       let firstLoadMax = this.maxCountRange;
299       if (this.mDuration / CommonConstants.MS_ONE_SECOND < this.maxCountRange) {
300         firstLoadMax = Math.ceil(this.mDuration / CommonConstants.MS_ONE_SECOND);
301       }
302
303       let callBack: ICallBack = {
304         callBackResult: (code: number) => {
305           if (code == 0) {
306             let count = 0;
307
308             let frameCallBack: IFrameCallBack = {
309               callBackResult: async (data: ArrayBuffer, timeUs: number) => {
310                 const imageSource: image.ImageSource = image.createImageSource(data);
311                 // TODO: 知识点: 如果要压缩分辨率,则加载第一张图时,获取视频的压缩分辨率相关参数信息,生成视频压缩命令
312                 if (this.videoTrimmerOption!.scaleNum! > 0 &&
313                   this.videoTrimmerOption!.scaleNum !== CommonConstants.SCALE_NUM &&
314                   this.imageWidth !== 0) {
315                   // 读取图片信息
316                   const imageInfo: image.ImageInfo = await imageSource!.getImageInfo();
317                   this.imageWidth = imageInfo.size.width;
318                   this.imageHeight = imageInfo.size.height;
319                   // 生成视频压缩命令
320                   this.scaleCmd =
321                     'scale=' +
322                     ((this.imageWidth / CommonConstants.SCALE_NUM) * this.videoTrimmerOption!.scaleNum!).toString() +
323                       ':' +
324                     ((this.imageHeight / CommonConstants.SCALE_NUM) * this.videoTrimmerOption!.scaleNum!).toString()
325                 }
326
327                 let videoThumbWidth: number = vp2px((this.mSeekBarWidth - 2 * this.thumbWidth) / this.maxCountRange);
328                 let videoThumbHeight: number = vp2px(this.mSeekBarHeight);
329                 let decodingOptions: image.DecodingOptions = {
330                   sampleSize: 1,
331                   editable: true,
332                   desiredSize: { width: videoThumbWidth, height: videoThumbHeight },
333                   desiredPixelFormat: image.PixelMapFormat.RGBA_8888
334                 };
335                 // TODO: 知识点: 在回调函数中循环生成小图,更新到小图列表中
336                 imageSource.createPixelMap(decodingOptions).then((px: image.PixelMap) => {
337                   let second = timeUs / CommonConstants.US_ONE_SECOND;
338                   let framePos = count;
339                   if (second == framePos) {
340                     logger.info(TAG,
341                       'framePos equal second, second=' + second + ' timeUs=' + timeUs + ' length=' + videoThumbs.length);
342                     videoThumbs[second].pixelMap = px;
343                     this.updateList(videoThumbs);
344                     count++;
345                     imageSource.release();
346                   } else {
347                     logger.info(TAG, 'framePos not equal second, framePos=' + framePos + ' timeUs=' + timeUs);
348                     videoThumbs[framePos].pixelMap = px;
349                     this.updateList(videoThumbs);
350                     count++;
351                     imageSource.release();
352                   }
353                   if (count == firstLoadMax) {
354                     this.videoTrimmerOption.loadFrameListener.onFinishLoad();
355                   }
356                 }).catch((err: Error) => {
357                   // 部分视频创建图片异常,直接返回
358                   console.error("createPixelMap Failed, err = " + err.name + ", message = " + err.message);
359                   this.videoTrimmerOption.loadFrameListener.onFinishLoad();
360                 })
361               }
362             }
363             // TODO: 知识点: 根据开始、结束时间,通过MP4Parser.getFrameAtTimeRang命令循环生成小图
364             let startTime = 0 * CommonConstants.US_ONE_SECOND + '';
365             let endTime = (firstLoadMax - 1) * CommonConstants.US_ONE_SECOND + '';
366            // 通过开始、结束时间,回调函数,获取视频小图
367             MP4Parser.getFrameAtTimeRang(startTime, endTime, MP4Parser.OPTION_CLOSEST, frameCallBack);
368           } else {
369             logger.info(TAG, 'setDataSource fail, error code =' + code.toString());
370           }
371         }
372       }
373       this.videoTrimmerOption.loadFrameListener.onStartLoad();
374       // 设置视频源和回调函数
375       MP4Parser.setDataSource(this.videoTrimmerOption.srcFilePath, callBack);
376       logger.info(TAG, "initList list size=" + videoThumbs.length);
377       this.updateList(videoThumbs);
378     }
379     ```
380
3817. 在视频剪辑界面,拉动左右进度条,根据拉动后的位置,计算选择要剪辑的时间范围,源码参考[RangeSeekBarView.ets](./entry/src/main/ets/videotrimmer/RangeSeekBarView.ets)。
382获取拉动位置:
383    ```typescript
384    .onActionUpdate((event?: GestureEvent) => {
385      let touchXNew: number = this.clearUndefined(event?.offsetX);
386      let deltax: number = touchXNew - this.touchXOld;
387      // 左边滑块移动
388      if (this.touchStatus == this.touch_left_thumb) {
389        this.leftThumbUpdate(deltax);
390      } else if (this.touchStatus == this.touch_right_thumb) {
391        // 右边滑块移动
392        this.rightThumbUpdate(deltax);
393      } else if (this.touchStatus == this.touch_hor_scroll) {
394        this.scrollUpdate(deltax);
395      }
396      this.touchDeltaX = deltax;
397      this.touchXOld = this.clearUndefined(event?.offsetX);
398    })
399    .onActionEnd((event?: GestureEvent) => {
400      this.touchStatus = this.touch_hor_scroll;
401      this.onRangeStopScrollChanged();
402    })
403    ```
404   根据拉动位置计算选择的时间段:
405    ```typescript
406   // 选取时间变动事件
407   onRangeValueChanged() {
408     let x0: number = this.scroller.currentOffset().xOffset;
409     let start: number = x0 + this.leftThumbRect[2] - this.leftThumbWidth;
410     let end: number = start + this.transparentWidth;
411
412     let startTime: number = start * CommonConstants.US_ONE_SECOND / this.msPxAvg;
413     this.leftText = this.showThumbText(startTime);
414     let endTime: number = end * CommonConstants.US_ONE_SECOND / this.msPxAvg;
415     this.rightText = this.showThumbText(endTime);
416
417     if (this.mRangSeekBarListener) {
418       this.mRangSeekBarListener.onRangeSeekBarValuesChanged(startTime, endTime);
419     }
420   }
421   ```
422
4238. 点击完成按钮,使用MP4Parser.ffmpegCmd接口,根据选择的起始时间和视频压缩命令,对视频进行压缩和剪辑,并保存到本地沙箱。源码参考[VideoTrimmerView.ets](./entry/src/main/ets/videotrimmer/VideoTrimmerView.ets)。
424
425   ```typescript
426	  // 最后一帧保护
427	  let sTime1 = 0;
428	  let eTime1 = 0;
429	  let lastFrameTime =
430		Math.floor(this.mDuration / CommonConstants.MS_ONE_SECOND) * CommonConstants.MS_ONE_SECOND;
431	  if (this.endTruncationTime > lastFrameTime) {
432		eTime1 = lastFrameTime;
433		sTime1 = eTime1 - (this.endTruncationTime - this.startTruncationTime);
434		eTime1 += CommonConstants.MS_ONE_SECOND; // 补偿1s
435	  } else {
436		sTime1 = this.startTruncationTime;
437		eTime1 = this.endTruncationTime;
438	  }
439	  // 选取的裁剪时间段
440	  let sTime = TimeUtils.msToHHMMSS(sTime1);
441	  let eTime = TimeUtils.msToHHMMSS(eTime1);
442      // 原视频
443	  let srcFilePath = this.videoTrimmerOption.srcFilePath;
444	  // 保留原来的文件名
445	  let fileName: string | undefined = srcFilePath!.split('/').pop()!.split('.')[0];
446	  // 组装剪辑后的文件路径(以 原来的文件名+当前时间 命名)
447	  let outFilePath: string =
448		getContext(this).cacheDir + '/' + fileName + '_' + TimeUtils.format(new Date()) + '.mp4';
449	  // 剪辑回调函数
450	  let callback: ICallBack = {
451		callBackResult: (code: number) => {
452		  if (code == 0) {
453			if (this.videoTrimmerOption) {
454			  // 通知上层调用
455			  this.videoTrimmerOption.listener.onFinishTrim(outFilePath);
456			  this.isTrimming = false;
457			}
458          }
459		}
460	  }
461	  // TODO: // 知识点: 根据开始、结束时间,视频源以及目标文件地址对视频进行剪辑
462	  this.videoClip(sTime, eTime, srcFilePath, outFilePath, this.scaleCmd, callback);
463   ```
464
465    ```typescript
466      // TODO: 知识点: 视频剪辑。  scaleCmd为视频压缩命令
467    videoClip(sTime: string, eTime: string, srcFilePath: string, outFilePath: string, scaleCmd: string,
468    callback: ICallBack) {
469      let ffmpegCmd: string = '';
470      if (scaleCmd !== '') {
471        ffmpegCmd =
472          'ffmpeg -y -i ' + srcFilePath + ' -vf ' + scaleCmd + ' -c:v mpeg4 -c:a aac -ss ' + sTime + ' -to ' + eTime +
473            ' ' + outFilePath;
474      } else {
475        ffmpegCmd = 'ffmpeg -y -i ' + srcFilePath + ' -c:v mpeg4 -c:a aac -ss ' + sTime + ' -to ' + eTime +
476          ' ' + outFilePath;
477      }
478      logger.info(TAG, 'videoClip cmd: ' + ffmpegCmd);
479      MP4Parser.ffmpegCmd(ffmpegCmd, callback);
480    }
481    ```
482
483**视频上传:**
484视频剪辑成功后自动跳转到视频上传界面。点击右上角的“保存”按钮,如果检测到没有配置服务器,则弹窗提示设置服务器地址,设置后重新点击右上角“保存”按钮,上传视频到服务器进行保存。
4851. 参照[environment](./environment/README.md)配置服务器地址,视频剪辑完成后点击“保存”,视频将上传到该服务器地址。
486
4872. 点击保存时,检测是否配置了服务器。源码参考[VideoUpload.ets](./entry/src/main/ets/pages/VideoUpload.ets)。
488   ```typescript
489   // 获取服务器地址
490   this.serverUrl = await urlUtils.getUrl(getContext(this) as common.UIAbilityContext);
491   if (this.serverUrl === undefined || this.serverUrl === '') {
492     // 打开自定义对话框,配置服务器地址
493     await this.customSetServerDlg.open();
494   }
495   this.serverUrl = await urlUtils.getUrl(getContext(this) as common.UIAbilityContext);
496   logger.info(TAG, 'serverUrl is = ' + this.serverUrl);
497   if (this.serverUrl !== undefined && this.serverUrl.length > 1) {
498     return true;
499   } else {
500     return false;
501   }
502   ```
503
5043. 定义上传配置参数 request.agent.Config。源码参考[RequestUpload.ets](./entry/src/main/ets/uploadanddownload/RequestUpload.ets)。
505    ```typescript
506    private config: request.agent.Config = {
507      action: request.agent.Action.UPLOAD,  // 上传
508      headers: HEADER,
509      url: '',                              // 上传服务器地址
510      mode: request.agent.Mode.FOREGROUND,  // 前台方式
511      method: 'POST',
512      title: 'upload',
513      network: request.agent.Network.ANY,   // 网络类型
514      data: [],
515      token: UPLOAD_TOKEN
516    }
517   ```
518
5194. 根据配置创建上传任务,并监听上传进度。当上传完成后通过callback进行回调通知,并删除上传任务。源码参考[RequestUpload.ets](./entry/src/main/ets/uploadanddownload/RequestUpload.ets)。
520    ```typescript
521    private uploadTask: request.agent.Task | undefined = undefined; // 上传任务
522
523      // 获取本地上传文件
524    this.config.data = await this.getFilesAndData(context.cacheDir, fileUris);
525    // TODO : 知识点 将视频上传到服务器地址
526    // 获取服务器地址
527    this.config.url = await urlUtils.getUrl(context);
528    // 前台模式
529    this.config.mode = request.agent.Mode.FOREGROUND;
530    try {
531      // 创建上传任务
532      this.uploadTask = await request.agent.create(context, this.config);
533      logger.info(TAG, `create uploadTask success, TaskID= ${this.uploadTask.tid}`);
534      // 监听上传进度
535      this.uploadTask.on('progress', this.progressCallback = (progress: request.agent.Progress) => {
536        logger.info(TAG, `progress,  progress = ${progress.processed} ${progress.state}`);
537        let processed = Number(progress.processed.toString()).valueOf();
538        let size = progress.sizes[0];
539        let process: number = Math.floor(processed / size * CommonConstants.PROGRESS_MAX);
540        if (process < CommonConstants.PROGRESS_MAX) {
541          // 进度通知
542          callback(process, false);
543        }
544      });
545      // 下载完成事件
546      this.uploadTask.on('completed', this.completedCallback = (progress: request.agent.Progress) => {
547        logger.info(TAG, `complete,  progress = ${progress.processed}, state= ${progress.state}`);
548        // 通知下载完成
549        callback(CommonConstants.PROGRESS_MAX, true);
550        // 删除任务
551        this.deleteTask();
552      });
553   }
554   ```
555
5565. 视频上传成功后,删除本地裁剪的视频,然后返回视频列表首页。源码参考[VideoUpload.ets](./entry/src/main/ets/pages/VideoUpload.ets)。
557    ```typescript
558    // 上传成功后删除裁剪的视频
559    fs.unlinkSync(this.workItem.videoSrc);
560   // 视频地址替换为服务器上的视频
561   this.workItem.videoSrc = this.serverUrl + this.workItem.videoSrc.split('/').pop();
562
563   //  保存首页背景图
564   async saveFirstImage() {
565     const url = await savePixelMap(getContext(this), this.workItem.firstImage as PixelMap, getTimeStr());
566     this.workItem.firstImage = fileUri.getUriFromPath(url);
567     this.workItem.date = TimeUtils.getCurrentTime();
568     // 通知视频列表首页更新上传的数据
569     AppStorage.setOrCreate('addWorkItem', this.workItem);
570     await this.customSetServerDlg.close();
571     DynamicsRouter.popAppRouter();
572   }
573   ```
574
5756. 视频列表首页监听“addWorkItem”数据,更新上传的视频数据。源码参考[VideoTrimmer.ets](./entry/src/main/ets/pages/VideoTrimmer.ets)。
576    ```typescript
577   @StorageLink('addWorkItem') @Watch('getUploadWorkItem') addWorkItem: WorkItem =
578     new WorkItem('', '', '', '', '', true); // 上传到服务器的视频信息
579
580   // 获取并添加新发表的视频信息
581   getUploadWorkItem(): void {
582     if (this.addWorkItem.videoSrc !== undefined && this.addWorkItem.videoSrc !== '') {
583       this.workList.addData(0, this.addWorkItem);
584       AppStorage.setOrCreate('addWorkItem', new WorkItem('', '', '', '', '', false));
585     }
586   }
587   ```
588
589### 高性能知识点
590
591本示例使用了[LazyForEach](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md) 进行数据懒加载优化,以降低内存占用和渲染开销。
592
593### 工程结构&模块类型
594
595   ```
596videotrimmer                                 // har类型
597|---constants                                // 常量
598|   |---Constants.ets                        // 通用常量
599|   |---DoanloadConstants.ets                // 下载常量
600|---customcomponents                         // 自定义组件
601|   |---CustomLoadingProgressDlg.ets         // 加载进度
602|   |---CustomSetServerDlg.ets               // 设置上传下载服务器
603|   |---CustomShareDlg.ets                   // 视频分享
604|   |---CustomTabBar.ets                     // 首页底部自定义组件
605|---model
606|   |---ShareInfo.ets                        // 分享信息数据
607|   |---TabBarModel.ets                      // Tab组件信息
608|   |---WorkListDataSource.ets               // IDataSource处理数据监听的基本实现
609|   |---WorkItemModel.ets                    // 单条视频信息模型
610|---pages
611|   |---VideoDetail.ets                      // 视图层-视频播放界面
612|   |---VideoTrimmer.ets                     // 视图层-应用主页面
613|   |---VideoUpload.ets                      // 视图层-视频保存上传界面
614|---uploadanddownload                        // 上传下载
615|   |---RequestDownload.ets                  // 下载类
616|   |---VideoUpload.ets                      // 上传类
617|---utils                                    // 通用工具
618|   |---FileUtil.ets                         // 文件处理
619|   |---Logger.ets                           // 日志
620|   |---TimeUtils.ets                        // 时间处理
621|   |---UrlUtils.ets                         // 服务器地址处理
622|---videoplaycomponents                      // 视频播放组件
623|   |---VideoPlayController.ets              // 视频控制器
624|   |---VideoPlayListener.ets                // 视频播放侦听回调
625|   |---XComponentVideo.ets                  // 视频播放组件
626|---videotrimmer                             // 视频裁剪组件
627|   |---RangeSeekBarView.ets                 // 视频剪辑时间长度选择组件
628|   |---RangSeekBarListener.ets              // 视频剪辑长度侦听接口
629|   |---RangSeekBarOption.ets                // 视频剪辑长度选项
630|   |---VideoLoadFramesListener.ets          // 视频帧加载回调接口
631|   |---VideoThumbListView.ets               // 视频帧图片列表预览组件
632|   |---VideoThumbOption.ets                 // 视频帧图片选项
633|   |---VideoTrimListener.ets                // 视频剪辑回调接口
634|   |---VideoTrimmerOption.ets               // 视频剪辑选项
635|   |---VideoTrimmerView.ets                 // 视频裁剪组件
636   ```
637
638### 模块依赖
639
640  依赖[mp4parser](https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fmp4parser)来使用FFmpeg命令。
641
642### 参考资料
643
644  [video_trimmer](https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fvideotrimmer)源码
645  [XComponent组件](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)
646  [mp4parser](https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Fmp4parser)
647  [上传下载](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-basic-services-kit/js-apis-request.md)
648
649### 相关权限
650
651| 权限名                                    | 权限说明                       | 级别         |
652|----------------------------------------|----------------------------| ------------ |
653| ohos.permission.INTERNET               | 允许访问网络。                    | normal       |
654| ohos.permission.READ_MEDIA     | 允许应用读取用户外部存储中的媒体文件信息。  | normal       |
655| ohos.permission.WRITE_MEDIA       | 允许应用读写用户外部存储中的媒体文件信息。 | normal |
656| ohos.permission.CAMERA | 允许应用使用相机。             | normal       |
657
658
659### 依赖
660
661- 不涉及
662
663### 约束与限制
664
6651.本示例仅支持标准系统上运行,支持设备:手机。
666
6672.本示例为Stage模型,支持API12版本SDK,SDK版本号(API Version 12 Release)。
668
6693.本示例需要使用DevEco Studio版本号(DevEco Studio 5.0.0 Release)及以上版本才可编译运行。
670
671
672### 下载
673如需单独下载本工程,执行如下命令:
674
675```
676git init
677git config core.sparsecheckout true
678echo code/BasicFeature/Media/videotrimmer/ > .git/info/sparse-checkout
679git remote add origin https://gitee.com/openharmony/applications_app_samples.git
680git pull origin master
681```