• 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 audio from '@ohos.multimedia.audio';
17import common from '@ohos.app.ability.common';
18import fs from '@ohos.file.fs';
19import image from '@ohos.multimedia.image';
20import media from '@ohos.multimedia.media';
21import { BusinessError } from '@ohos.base';
22import CommonConstants from '../constants/Constants';
23import { logger } from '../utils/Logger';
24// mp4parser 三方库组件
25import { IFrameCallBack, ICallBack, MP4Parser } from '@ohos/mp4parser';
26// 时间工具
27import { TimeUtils } from '../utils/TimeUtils';
28// 视频剪辑选项
29import { VideoTrimmerOption } from './VideoTrimmerOption';
30// 视频帧图片预览列表相关
31import { ThumbContent, VideoThumbOption } from './VideoThumbOption';
32import { VideoThumbListView } from './VideoThumbListView';
33// 视频剪辑选择条相关
34import { RangeSeekBarView, MediaPlayerStatus } from './RangeSeekBarView';
35import { RangSeekBarListener } from './RangSeekBarListener';
36import { RangSeekBarOption } from './RangSeekBarOption';
37
38const TAG: string = 'videoTrimmer_component';
39
40@Component
41export struct VideoTrimmerView {
42  @Prop @Watch('watchVideoTrimmerOption') videoTrimmerOption: VideoTrimmerOption;
43
44  // 裁剪选项变动时停止播放刷新界面
45  watchVideoTrimmerOption() {
46    this.configUIInfo();
47    this.isPausePreview = true;
48    this.waitLoadedInitAVPlayer();
49  }
50
51  private isLoadFrame: boolean = false;  // 是否在加载帧数据
52  // 视频图片预览区选择区域相关参数
53  @State hintText?: string = '拖动选择你要发表的10秒以内片段';
54  // 播放进度条位置
55  private mRedProgressBarPos: number = 0;
56  // 滑动条相关变量
57  private mVideoParentHeight: number = 0;
58  private mVideoParentWidth: number = 0;
59  private mSeekBarWidth: number = 0;
60  private mSeekBarHeight: number = 0;
61  @State thumbWidth: number = 30; // 滑动宽度
62  private thumbLimitLow: number = 6; // 滑动最低限制
63  @State paddingLineHeight: number = 10; // 滑动条边框
64  private padLimitHigh: number = 20; // 滑动条边框最高限制
65  private padLimitLow: number = 2; // 滑动条边框最低限制
66  private thumbLimitRate: number = 0.15;
67  // 视频总时长
68  private mDuration: number = 0;
69  // 是否是restore
70  private isFromRestore: boolean = false;
71  private scroller: Scroller = new Scroller();
72  @State ratio: number = 1.0;
73  @State currentTime: string = '0';
74  @State durationTimeText: string = '100';
75  @State descriptionValue: string = '';
76  private avPlayer?: media.AVPlayer = undefined;
77  private surfaceID: string = '';
78  private mXcomponentController: XComponentController = new XComponentController();
79  // XComponent 加载状态
80  private isLoaded: boolean = false;
81  private isTrimming: boolean = false;
82  @State videoMaxTime: number = 8; // 8秒
83  @State videoMinTime: number = 3; // 最小剪辑时间3s
84  @State maxCountRange: number = 8; //seekBar的区域内一共有多少张图片
85  // 小图选项
86  @State mVideoThumbOption: VideoThumbOption = {
87    videoThumbWidth: 0,
88    videoThumbs: []
89  };
90  // 视频剪辑长度选项
91  @State mRangSeekBarOption: RangSeekBarOption = {
92    mLeftProgressPos: 0,
93    mRightProgressPos: 10 * 1000,
94    // 最小时间 比如一屏幕10张图 间隔10s, 现在最小时间是3s,那么leftThumb和rightThumb的最小间隔是3张图宽度
95    mMinShootTime: 3,
96    // 最大时间 约束了右侧thumb和左侧thumb的距离
97    mMaxShootTime: 10,
98    // 一屏幕展示最大图片数量, 一张图是1s,如果最大数量是20, 最大时间是10s,那么 rightThumb 距离 leftThumb 就是屏幕的10/20
99    // 需要保证mMaxCountRange <= mMaxShootTime 否则右侧的thumb会出界
100    mMaxCountRange: 10,
101    mThumbWidth: 30,
102    mPaddingTopBottom: 10
103  };
104  // 视频播放状态
105  @State mMediaPlayerStatus: MediaPlayerStatus = {
106    isPlaying: false,
107    redLineProgress: 0
108  };
109  // 剪辑初始时间
110  private startTruncationTime: number = 0;
111  // 剪辑结束时间
112  private endTruncationTime: number = 0;
113  // 视频分辨率信息
114  private imageWidth: number = 0;
115  private imageHeight: number = 0;
116  // 压缩命令
117  private scaleCmd: string = '';
118  @State mRangSeekBarListener: RangSeekBarListener = {
119    onRangeSeekBarValuesChanged: (minValue, maxValue) => {
120      // 视频裁剪 和 预览的时间范围
121      logger.info(TAG, 'RangSeekBarListener minValue=' + minValue + ', maxValue=' + maxValue);
122      this.pause();
123      this.isPausePreview = true;
124      // 裁剪时间段
125      this.startTruncationTime = Math.floor(minValue);
126      this.endTruncationTime = Math.floor(maxValue);
127
128      this.mMediaPlayerStatus = {
129        isPlaying: false,
130        redLineProgress: 0
131      }
132      this.seekTo(this.startTruncationTime);
133    },
134    onRangeSeekBarScrollChanged: (startPos, endPos) => {
135      // 其他操作正在加载帧数据的话,返回
136      if(this.isLoadFrame) {
137        logger.warn(TAG, 'other operate is LoadFrame, please wait a moment!');
138        return;
139      }
140      this.isLoadFrame = true;
141      logger.info(TAG,
142        'RangSeekBarListener onRangeSeekBarScrollChanged startPos=' + startPos + ' endPos=' + endPos);
143      let videoThumbs = this.mVideoThumbOption.videoThumbs;
144      if (videoThumbs == undefined) {
145        for (let i = 0; i < this.mDuration; i = i + CommonConstants.MS_ONE_SECOND) {
146          let temp = new ThumbContent();
147          if (this.videoTrimmerOption.framePlaceholder) {
148            temp.framePlaceholder = this.videoTrimmerOption.framePlaceholder;
149          }
150          if (this.videoTrimmerOption.frameBackground) {
151            temp.frameBackground = this.videoTrimmerOption.frameBackground;
152          }
153          videoThumbs.push(temp);
154        }
155      }
156
157      let reqStartPos = 0;
158      for (let i = startPos; i <= endPos; i = i + 1) {
159        if (i < videoThumbs.length && videoThumbs[i] && !videoThumbs[i].pixelMap) {
160          reqStartPos = i;
161          break;
162        } else {
163          continue;
164        }
165      }
166
167      if (reqStartPos > 0) {
168        this.videoTrimmerOption.loadFrameListener.onStartLoad()
169        let reqCount = endPos - reqStartPos + 1;
170        let count = 0;
171        let callBack: ICallBack = {
172          callBackResult: (code: number) => {
173            if (code === 0) {
174              // 请求新的帧数据
175              let frameCallBack: IFrameCallBack = {
176                callBackResult: async (data: ArrayBuffer, timeUs: number) => {
177                  const imageSource: image.ImageSource = image.createImageSource(data);
178
179                  let videoThumbWidth = vp2px((this.mSeekBarWidth - 2 * this.thumbWidth) / this.maxCountRange);
180                  let videoThumbHeight = vp2px(this.mSeekBarHeight);
181
182                  let decodingOptions: image.DecodingOptions = {
183                    sampleSize: 1,
184                    editable: true,
185                    desiredSize: { width: videoThumbWidth, height: videoThumbHeight },
186                    desiredPixelFormat: image.PixelMapFormat.RGBA_8888
187                  }
188                  imageSource.createPixelMap(decodingOptions).then(px => {
189                    let second = timeUs / CommonConstants.US_ONE_SECOND;
190                    let framePos = reqStartPos + count;
191                    if (second === framePos) {
192                      logger.info(TAG, 'second equal framePos, second=' + second + ' timeUs=' + timeUs + ' length=' +
193                      videoThumbs.length)
194                      videoThumbs[second].pixelMap = px;
195                      // 更新加载的视频帧图片
196                      this.updateList(videoThumbs);
197                      count++;
198                      imageSource.release();
199                    } else {
200
201                      logger.info(TAG, 'second not equal framePos,  framePos=' + framePos + ' timeUs=' + timeUs);
202                      videoThumbs[framePos].pixelMap = px;
203                      this.updateList(videoThumbs);
204                      count++;
205                      imageSource.release();
206                    }
207                    if (count == reqCount) {
208                      this.videoTrimmerOption.loadFrameListener.onFinishLoad();
209                      this.isLoadFrame = false;
210                    }
211                  }).catch((err: Error) => {
212                    // 部分视频创建图片异常,直接返回
213                    logger.error(TAG, 'createPixelMap Failed, err = ' + err.name + ', message = ' + err.message);
214                    this.videoTrimmerOption.loadFrameListener.onFinishLoad();
215                    this.isLoadFrame = false;
216                  })
217                }
218              }
219              let startTime = reqStartPos * CommonConstants.US_ONE_SECOND + '';
220              let endTime = endPos * CommonConstants.US_ONE_SECOND + '';
221              // TODO: 知识点: 截取所选时间段的视频
222              MP4Parser.getFrameAtTimeRang(startTime, endTime, MP4Parser.OPTION_CLOSEST, frameCallBack);
223            } else {
224              logger.error(TAG, 'setDataSource fail, error code =' + code.toString());
225              // 部分视频创建图片异常,直接返回
226              this.videoTrimmerOption.loadFrameListener.onFinishLoad();
227              this.isLoadFrame = false;
228            }
229          }
230        }
231        // TODO: 知识点: 设置视频源及回调
232        MP4Parser.setDataSource(this.videoTrimmerOption.srcFilePath, callBack);
233      }
234    }
235  }
236
237  // 释放播放器
238  releaseAvPlayer(): Promise<void> {
239    return new Promise((resolve, reject) => {
240      if (this && this.avPlayer) {
241        this.setOffCallback();
242        this.avPlayer?.stop().then(() => {
243          this.avPlayer?.release().then(() => {
244            resolve();
245          })
246            .catch((err: BusinessError) => {
247              reject(err);
248            })
249        })
250          .catch((err: BusinessError) => {
251            reject(err);
252          })
253      } else {
254        reject('this.avPlayer? is not init cannot stop and release!');
255      }
256    })
257  }
258
259  configUIInfo() {
260    if (this.videoTrimmerOption.videoMaxTime) {
261      this.videoMaxTime = this.videoTrimmerOption.videoMaxTime;
262    }
263    if (this.videoTrimmerOption.videoMinTime) {
264      this.videoMinTime = this.videoTrimmerOption.videoMinTime;
265    }
266    if (this.videoTrimmerOption.maxCountRange) {
267      this.maxCountRange = this.videoTrimmerOption.maxCountRange;
268    }
269    if (this.videoTrimmerOption.thumbWidth) {
270      this.thumbWidth = this.videoTrimmerOption.thumbWidth;
271    }
272    if (this.videoTrimmerOption.paddingLineHeight) {
273      this.paddingLineHeight = this.videoTrimmerOption.paddingLineHeight;
274    }
275
276    // 限制maxCountRange的图片最小宽度1vp
277    if (this.mSeekBarWidth > 0) {
278      let maxVp = (this.mSeekBarWidth - 2 * this.thumbWidth)
279      if (this.maxCountRange > maxVp) {
280        this.maxCountRange = maxVp;
281      }
282      // check 限制最大截取时间不超过 maxCountRange
283      if (this.videoMaxTime > this.maxCountRange) {
284        this.videoMaxTime = this.maxCountRange;
285      }
286    }
287    // 滑动条设置
288    if (this.mVideoParentWidth > 0) {
289      let thumbLimitHigh = this.mVideoParentWidth * this.thumbLimitRate;
290      if (this.thumbWidth > thumbLimitHigh) {
291        this.thumbWidth = thumbLimitHigh;
292      }
293      if (this.thumbWidth < this.thumbLimitLow) {
294        this.thumbWidth = this.thumbLimitLow;
295      }
296    }
297
298    // 滑动条上下边框设置
299    if (this.mVideoParentHeight > 0) {
300      if (this.paddingLineHeight < this.padLimitLow) {
301        this.paddingLineHeight = this.padLimitLow;
302      }
303      if (this.paddingLineHeight > this.padLimitHigh) {
304        this.paddingLineHeight = this.padLimitHigh;
305      }
306    }
307  }
308
309  // 界面销毁监听
310  async aboutToDisappear(): Promise<void> {
311    // 保证页面销毁 如果还有取帧行为应该及时停止该子线程操作
312    MP4Parser.stopGetFrame();
313    logger.info(TAG, 'aboutToDisappear success');
314    this.setOffCallback();
315    await this.avPlayer?.stop();
316    await this.avPlayer?.release();
317  }
318
319  // 设置播放时间上报监听
320  timeUpdate(): void {
321    this.avPlayer?.on('timeUpdate', (time: number) => {
322      if (!this.isPausePreview) {
323        // 播放占比情况
324        if (this.avPlayer != undefined) {
325          this.mRedProgressBarPos = (this.avPlayer?.currentTime - this.startTruncationTime) * (1.0) /
326            (this.endTruncationTime - this.startTruncationTime);
327        }
328        if (this.mRedProgressBarPos <= 0) {
329          this.mRedProgressBarPos = 0;
330        }
331        if (this.mRedProgressBarPos >= 1) {
332          this.mRedProgressBarPos = 1;
333        }
334        this.mMediaPlayerStatus = {
335          isPlaying: true,
336          redLineProgress: this.mRedProgressBarPos
337        };
338      }
339      // 当播放的时间大于等于截止时间时候 停止播放
340      if (this.avPlayer != undefined && this.avPlayer.currentTime >= this.endTruncationTime) {
341        this.isPausePreview = true;
342        this.pause();
343        this.seekTo(this.startTruncationTime);
344        this.mMediaPlayerStatus = {
345          isPlaying: false,
346          redLineProgress: 0
347        };
348      }
349    })
350  }
351
352  // 设置错误监听
353  setErrorCallback(): void {
354    this.avPlayer?.on('error', (error) => {
355      // 输出错误日志
356      logger.info(TAG, 'error happened,message is :' + error.message);
357    })
358  }
359
360  // 注销回调函数接口
361  setOffCallback(): void {
362    if (this && this.avPlayer) {
363      this.avPlayer?.off('volumeChange');
364      this.avPlayer?.off('endOfStream');
365      this.avPlayer?.off('seekDone');
366      this.avPlayer?.off('durationUpdate');
367      this.avPlayer?.off('speedDone');
368      this.avPlayer?.off('bitrateDone');
369      this.avPlayer?.off('bufferingUpdate');
370      this.avPlayer?.off('startRenderFrame');
371      this.avPlayer?.off('videoSizeChange');
372      this.avPlayer?.off('audioInterrupt');
373      this.avPlayer?.off('availableBitrates');
374      this.avPlayer?.off('error');
375      this.avPlayer?.off('stateChange');
376    }
377  }
378
379  //设置播放surfaceID,播放音频时无需设置
380  setSurfaceID(): void {
381    if (this && this.avPlayer) {
382      this.avPlayer.surfaceId = this.surfaceID;
383    }
384  }
385
386  // 视频信息上报函数
387  async setSourceInfo(): Promise<void> {
388    if (this && this.avPlayer) {
389      // 音量变化回调函数
390      this.avPlayer?.on('volumeChange', (vol: number) => {
391        logger.info(TAG, 'volumeChange success,and new volume is :' + vol);
392      });
393      // 视频播放结束触发回调
394      this.avPlayer?.on('endOfStream', () => {
395        logger.info(TAG, 'endOfStream success');
396      });
397      // seek操作回调函数
398      this.avPlayer?.on('seekDone', (seekDoneTime: number) => {
399        logger.info(TAG, 'seekDone success,and seek time is:' + seekDoneTime);
400      });
401      // 视频总时长上报函数
402      this.avPlayer?.on('durationUpdate', (duration: number) => {
403        logger.info(TAG, 'durationUpdate success,and durationUpdate is:' + duration);
404      });
405      // 设置倍速播放回调函数
406      this.avPlayer?.on('speedDone', (speed: number) => {
407        logger.info(TAG, 'speedDone success,and speed value is:' + speed + ', speed state is :' + this.getState());
408      });
409      // bitrate设置成功回调函数
410      this.avPlayer?.on('bitrateDone', (bitrate: number) => {
411        logger.info(TAG, 'bitrateDone success,and bitrate value is:' + bitrate);
412      });
413      // 缓冲上报回调函数
414      this.avPlayer?.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
415        logger.info(TAG, 'bufferingUpdate success,and infoType value is:' + infoType + ', value is :' + value);
416      });
417      // 首帧上报回调函数
418      this.avPlayer?.on('startRenderFrame', () => {
419        logger.info(TAG, 'startRenderFrame success');
420      });
421      // 视频宽高上报回调函数
422      this.avPlayer?.on('videoSizeChange', (width: number, height: number) => {
423        logger.info(TAG, 'videoSizeChange success,and width is:' + width + ', height is :' + height);
424      });
425      // 焦点上报回调函数
426      this.avPlayer?.on('audioInterrupt', (info: audio.InterruptEvent) => {
427        // 触发焦点上报后调用暂停接口暂停播放
428        this.pause();
429        logger.info(TAG, 'audioInterrupt success,and InterruptEvent info is:' + JSON.stringify(info));
430      });
431      // HLS上报所有支持的比特率
432      this.avPlayer?.on('availableBitrates', (bitrates: Array<number>) => {
433        logger.info(TAG, 'availableBitrates success,and availableBitrates length is:' + bitrates.length);
434      });
435    }
436  }
437
438  isNext = true;
439
440  // 状态机上报回调函数
441  async setStateChangeCallback(): Promise<void> {
442    if (this && this.avPlayer) {
443      this.avPlayer?.on('stateChange', async (state, reason) => {
444
445        logger.info(TAG, 'stateChange callback function is triggered,state is:' + state + ',reason is :' + reason);
446        switch (state) {
447          case 'idle':
448            logger.info(TAG, 'state idle called');
449            break;
450          case 'initialized':
451            logger.info(TAG, 'state initialized called');
452            if (this.isNext) {
453              this.setSurfaceID();
454              await this.avPlayer?.prepare();
455            }
456            break;
457          case 'prepared':
458            // 设置焦点上报类型
459            if (this.avPlayer != undefined) {
460              this.avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE;
461            }
462            this.videoPrepared()
463            logger.info(TAG, 'state prepared called :' + this.getCurrentTime());
464            if (this.isPausePreview) {
465              this.play();
466            }
467            this.isNext = false;
468            break;
469          case 'playing':
470            logger.info(TAG, 'state playing called');
471            break;
472          case 'paused':
473            logger.info(TAG, 'state paused called');
474            break;
475          case 'completed':
476            logger.info(TAG, 'state completed called');
477            this.isPausePreview = true;
478            this.pause();
479            this.seekTo(this.startTruncationTime)
480            this.mRedProgressBarPos = 0
481            this.mMediaPlayerStatus = {
482              isPlaying: false,
483              redLineProgress: this.mRedProgressBarPos
484            };
485            break;
486          case 'stopped':
487            logger.info(TAG, 'state stopped called');
488            break;
489          case 'released':
490            logger.info(TAG, 'state released called');
491            break;
492          case 'error':
493            logger.info(TAG, 'state error called');
494            break;
495          default:
496            logger.info(TAG, 'unkown state :' + state);
497            break;
498        }
499      })
500    }
501  }
502
503  // 创建AVPlayer实例对象
504  async createAVPlayer(): Promise<boolean> {
505    logger.info(TAG, 'createAVPlayer start');
506    let ret = false;
507    if (this.avPlayer !== undefined) {
508      await this.avPlayer?.release();
509      this.avPlayer = undefined;
510    }
511    this.avPlayer = await media.createAVPlayer();
512    if (this.avPlayer !== undefined) {
513      ret = true;
514    }
515    this.avPlayer.url = this.fdPath;
516    logger.info(TAG, 'createAVPlayer end');
517    return ret;
518  }
519
520  // 调用播放接口
521  async play(): Promise<void> {
522    logger.info(TAG, 'start to play');
523    if (this.avPlayer) {
524      logger.info(TAG, 'start to play1');
525      this.avPlayer?.play();
526      logger.info(TAG, 'start to play2');
527    } else {
528      logger.info(TAG, ' pause() must init avplayer');
529    }
530  }
531
532  // 暂停接口
533  pause(): void {
534    logger.info(TAG, 'start to pause');
535    if (this.avPlayer) {
536      this.avPlayer?.pause();
537    } else {
538      logger.info(TAG, ' pause() must init avplayer');
539    }
540  }
541
542  // 设置倍数接口
543  setSpeed(speedValue: number): void {
544    logger.info(TAG, 'set speed value is:' + speedValue);
545    switch (speedValue) {
546      case 0:
547        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_0_75_X);
548        break;
549      case 1:
550        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
551        break;
552      case 2:
553        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
554        break;
555      case 3:
556        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
557        break;
558      case 4:
559        this.avPlayer?.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
560        break;
561      default:
562        logger.info(TAG, 'no this mode speed:' + speedValue);
563        break;
564    }
565  }
566
567  // 获取当前播放时间函数
568  getCurrentTime(): number {
569    if (this.avPlayer != undefined) {
570      return this.avPlayer?.currentTime;
571    }
572    return 0;
573  }
574
575  // 获取当前播放状态函数
576  getState(): string {
577    if (this.avPlayer != undefined) {
578      return this.avPlayer.state;
579    } else {
580      return 'error';
581    }
582  }
583
584  // 设置loop函数
585  setLoop(loopValue: boolean): void {
586    if (this.avPlayer != undefined) {
587      this.avPlayer.loop = loopValue;
588    }
589  }
590
591  private onReadNext?: () => void = () => {
592  }
593
594  private runNextFunction(nextFunction: () => void) {
595    if (!this.isLoaded) {
596      this.onReadNext = nextFunction;
597    } else {
598      nextFunction();
599    }
600  }
601
602  async waitLoadedInitAVPlayer(): Promise<void> {
603    this.runNextFunction(this.initAVPlayer);
604  }
605
606  private fdHead: string = 'fd://';
607  private fdPath: string = '';
608  private fd: number = -1;
609  // 初始化函数
610  initAVPlayer: () => Promise<void> = async () => {
611    logger.info(TAG, 'initAVPlayer success');
612    let filePath = '';
613    if (this.videoTrimmerOption) {
614      filePath = this.videoTrimmerOption.srcFilePath;
615    }
616    if (filePath.length <= 0) {
617      return;
618    }
619
620    this.fd = fs.openSync(filePath, fs.OpenMode.READ_ONLY).fd;
621    this.fdPath = this.fdHead + this.fd;
622    logger.info(TAG, 'filePath=' + filePath + 'videoFd = ' + this.fdPath);
623    this.surfaceID = this.mXcomponentController.getXComponentSurfaceId();
624    logger.info(TAG, 'surfaceID is : ' + this.surfaceID);
625    await this.createAVPlayer();
626    this.setStateChangeCallback();
627    this.setErrorCallback();
628    this.timeUpdate();
629    this.setSourceInfo();
630  }
631
632  // 界面初始化函数
633  aboutToAppear() {
634    this.configUIInfo();
635    this.isPausePreview = true;
636    this.waitLoadedInitAVPlayer();
637  }
638
639  onBackPress() {
640    this.releaseAvPlayer().then(() => {
641      // 释放avPlayer 无需后续操作
642    })
643  }
644
645  build() {
646    Column() {
647      // 视频窗口
648      this.videoContent();
649      // 底部裁剪区
650      this.bottomContent();
651    }.width($r('app.string.video_trimmer_full_size'))
652    .height($r('app.string.video_trimmer_full_size'))
653    .backgroundColor(Color.Black)
654  }
655
656  @Builder
657  private videoContent() {
658    Stack({ alignContent: Alignment.Center }) {
659      XComponent({
660        id: 'componentId',
661        type: XComponentType.SURFACE,
662        controller: this.mXcomponentController
663      })
664        .onLoad(async () => {
665          this.isLoaded = true;
666          // 加载完成后调用初始化播放器函数
667          if (this.onReadNext) {
668            this.onReadNext();
669            this.onReadNext = undefined
670          }
671        })
672        .aspectRatio(this.ratio)
673    }
674    .onAreaChange((oldValue, newValue) => {
675      this.mVideoParentWidth = newValue.width as number;
676      this.mVideoParentHeight = newValue.height as number;
677    })
678    .width($r('app.string.video_trimmer_full_size'))
679    .height('75%')
680  }
681
682  @Builder
683  private bottomContent() {
684    Column() {
685      // Text(`拖动选择你要发表的${this.videoMaxTime}秒以内片段`)
686      Text(this.hintText)
687        .fontColor(Color.White)
688        .height($r('app.integer.video_trimmer_hit_height'))
689      this.userSelectContent()
690      this.bottomClickContent()
691    }
692    .alignItems(HorizontalAlign.Center)
693    .width($r('app.string.video_trimmer_full_size'))
694    .height($r('app.string.video_trimmer_bottom_height'))
695  }
696
697  @State isPausePreview: boolean = true;
698
699  @Builder
700  private bottomClickContent() {
701    Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
702      Text($r('app.string.video_trimmer_cancel'))
703        .id('txt_trimmer_cancel')
704        .fontColor(Color.White)
705        .textAlign(TextAlign.Center)
706        .width($r('app.integer.video_trimmer_btn_width'))
707        .height($r('app.integer.video_trimmer_btn_height'))
708
709        .onClick(() => {
710          if (this.videoTrimmerOption) {
711            this.releaseAvPlayer().then(() => {
712              this.videoTrimmerOption.listener.onCancel()
713            }).catch((err: BusinessError) => {
714
715              logger.info(TAG, 'VideoTrimmerView onCancel avplayer.stop() Or avplayer.release() err=' +
716              (err as BusinessError).message)
717              this.videoTrimmerOption.listener.onCancel()
718            })
719          }
720        })
721
722      Image(this.isPausePreview ? $r('app.media.video_trimmer_icon_pause') : $r('app.media.video_trimmer_icon_play'))
723        .id('txt_trimmer_play')
724        .height($r('app.integer.video_trimmer_video_item_news_image_height'))
725        .width($r('app.integer.video_trimmer_video_item_news_image_height'))
726        .objectFit(ImageFit.Contain)
727        .onClick(() => {
728          // 改变状态
729          this.isPausePreview = !this.isPausePreview;
730
731          if (this.isPausePreview) {
732            //  暂停
733            this.mMediaPlayerStatus = {
734              isPlaying: false,
735              redLineProgress: this.mRedProgressBarPos
736            };
737            // 暂停视频播放
738            this.pause();
739          } else {
740            // 开始预览视频
741            this.play();
742          }
743        })
744
745      Text($r('app.string.video_trimmer_finish'))
746        .fontColor($r('app.color.video_trimmer_finish_color'))
747        .id('txt_trimmer_finish')
748        .textAlign(TextAlign.Center)
749        .width($r('app.integer.video_trimmer_btn_width'))
750        .height($r('app.integer.video_trimmer_btn_height'))
751        .onClick(() => {
752          if (this.isTrimming) {
753            return;
754          }
755          this.isTrimming = true;
756          if (this.videoTrimmerOption) {
757            // 开始裁剪
758            this.videoTrimmerOption.listener.onStartTrim();
759          }
760
761          // 最后一帧保护
762          let sTime1 = 0;
763          let eTime1 = 0;
764          let lastFrameTime =
765            Math.floor(this.mDuration / CommonConstants.MS_ONE_SECOND) * CommonConstants.MS_ONE_SECOND;
766          if (this.endTruncationTime > lastFrameTime) {
767            eTime1 = lastFrameTime;
768            sTime1 = eTime1 - (this.endTruncationTime - this.startTruncationTime);
769            eTime1 += CommonConstants.MS_ONE_SECOND; // 补偿1s
770          } else {
771            sTime1 = this.startTruncationTime;
772            eTime1 = this.endTruncationTime;
773          }
774          // 选取的裁剪时间段
775          let sTime = TimeUtils.msToHHMMSS(sTime1);
776          let eTime = TimeUtils.msToHHMMSS(eTime1);
777
778          let srcFilePath = this.videoTrimmerOption.srcFilePath;
779          // 保留原来的文件名
780          let fileName: string | undefined = srcFilePath!.split('/').pop()!.split('.')[0];
781          // 组装剪辑后的文件路径(以 原来的文件名+当前时间 命名)
782          let outFilePath: string =
783            getContext(this).cacheDir + '/' + fileName + '_' + TimeUtils.format(new Date()) + '.mp4';
784          // 剪辑回调函数
785          let callback: ICallBack = {
786            callBackResult: (code: number) => {
787              if (code === 0) {
788                if (this.videoTrimmerOption) {
789                  // 通知上层调用
790                  this.videoTrimmerOption.listener.onFinishTrim(outFilePath);
791                  this.isTrimming = false;
792                }
793              }
794            }
795          }
796          // TODO: // 知识点: 根据开始、结束时间,视频源以及目标文件地址对视频进行剪辑
797          this.videoClip(sTime, eTime, srcFilePath, outFilePath, this.scaleCmd, callback);
798        })
799    }
800    .width($r('app.string.video_trimmer_full_size'))
801    .height($r('app.string.video_trimmer_half_height'))
802  }
803
804  /**
805   * 用户选择视频事件范围区域
806   */
807  @Builder
808  private userSelectContent() {
809    Stack() {
810      Column() {
811        Stack() {
812        }
813        .width($r('app.string.video_trimmer_full_size'))
814        .height($r('app.string.video_trimmer_rangeSeekBar_height'))
815
816        // 加载的视频帧图片列表组件
817        VideoThumbListView({ scroller: this.scroller, mVideoThumbOption: this.mVideoThumbOption })
818          .width($r('app.string.video_trimmer_full_size'))
819          .height($r('app.string.video_trimmer_list_height'))
820      }.margin({ left: this.thumbWidth, right: this.thumbWidth })
821
822      // 视频剪辑时间长度选择组件
823      RangeSeekBarView({
824        scroller: this.scroller,
825        mRangSeekBarOption: this.mRangSeekBarOption,
826        mMediaPlayerStatus: this.mMediaPlayerStatus,
827        mRangSeekBarListener: this.mRangSeekBarListener
828      })
829        .width($r('app.string.video_trimmer_full_size'))
830        .height($r('app.string.video_trimmer_full_size'))
831        .onAreaChange((oldValue, newValue) => {
832          this.mSeekBarWidth = newValue.width as number;
833          this.mSeekBarHeight = newValue.height as number;
834          this.configUIInfo();
835        })
836    }
837    .width($r('app.string.video_trimmer_full_size'))
838    .height($r('app.integer.video_trimmer_user_select_height'))
839  }
840
841  videoPrepared() {
842    // 提示语设置
843    this.hintText = this.getFormatString($r('app.string.video_trimmer_hint'), `${this.videoMaxTime}`)
844    // 宽高比 设置
845    if (this.avPlayer != undefined) {
846      let videoWidth = this.avPlayer.width;
847      let videoHeight = this.avPlayer.height;
848      this.ratio = videoWidth * (1.0) / videoHeight;
849      logger.info(TAG, 'this.ratio=' + this.ratio);
850    }
851
852    // 设置当前Video 总时长
853    if (this.avPlayer != undefined) {
854      this.mDuration = this.avPlayer.duration;
855      logger.info(TAG, 'this.mDuration =' + this.mDuration.toString());
856    }
857    // 默认的截取时间段[0-Nath.min(videoMaxTime,mDuration)]
858    this.startTruncationTime = 0;
859    this.endTruncationTime = Math.min(this.videoMaxTime * CommonConstants.MS_ONE_SECOND, this.mDuration);
860
861    if (!this.getRestoreState()) {
862      this.seekTo(this.mRedProgressBarPos);
863    } else {
864      this.setRestoreState(false);
865      this.seekTo(this.mRedProgressBarPos);
866    }
867    // 初始化剪辑区域图片列表
868    this.initImageList();
869    // 初始化剪辑区域
870    this.initRangeSeekBarView();
871  }
872
873  /**
874   * 初始化剪辑区域图片列表
875   */
876  initImageList() {
877    // 将视频长度分割为一秒一张图片
878    let videoThumbs: ThumbContent[] = [];
879    for (let i = 0; i < this.mDuration; i = i + CommonConstants.MS_ONE_SECOND) {
880      let temp = new ThumbContent();
881
882      if (this.videoTrimmerOption.framePlaceholder) {
883        temp.framePlaceholder = this.videoTrimmerOption.framePlaceholder;
884      }
885      if (this.videoTrimmerOption.frameBackground) {
886        temp.frameBackground = this.videoTrimmerOption.frameBackground;
887      }
888      videoThumbs.push(temp);
889    }
890
891    let firstLoadMax = this.maxCountRange;
892    if (this.mDuration / CommonConstants.MS_ONE_SECOND < this.maxCountRange) {
893      firstLoadMax = Math.ceil(this.mDuration / CommonConstants.MS_ONE_SECOND);
894    }
895
896    this.isLoadFrame = true;
897    let callBack: ICallBack = {
898      callBackResult: (code: number) => {
899        if (code === 0) {
900          let count = 0;
901
902          let frameCallBack: IFrameCallBack = {
903            callBackResult: async (data: ArrayBuffer, timeUs: number) => {
904              const imageSource: image.ImageSource = image.createImageSource(data);
905              // TODO: 知识点: 如果要压缩分辨率,则加载第一张图时(根据this.imageWidth === 0做判断),获取视频的压缩分辨率相关参数信息,生成视频压缩命令
906              if (this.videoTrimmerOption!.scaleNum! > 0 &&
907                this.videoTrimmerOption!.scaleNum !== CommonConstants.SCALE_NUM &&
908                this.imageWidth === 0) {
909                // 读取图片信息
910                const imageInfo: image.ImageInfo = await imageSource!.getImageInfo();
911                this.imageWidth = imageInfo.size.width;
912                this.imageHeight = imageInfo.size.height;
913                // 生成视频压缩命令
914                this.scaleCmd =
915                  'scale=' +
916                  ((this.imageWidth / CommonConstants.SCALE_NUM) * this.videoTrimmerOption!.scaleNum!).toString() +
917                    ':' +
918                  ((this.imageHeight / CommonConstants.SCALE_NUM) * this.videoTrimmerOption!.scaleNum!).toString()
919                logger.info(TAG,
920                  'init imageSize width = ' + this.imageWidth.toString() + ' height =' + this.imageHeight.toString() +
921                    ', scaleNum = ' + this.videoTrimmerOption!.scaleNum?.toString());
922              }
923              // 根据SeekBar尺寸生成小图参数
924              let videoThumbWidth: number = vp2px((this.mSeekBarWidth - 2 * this.thumbWidth) / this.maxCountRange);
925              let videoThumbHeight: number = vp2px(this.mSeekBarHeight);
926              let decodingOptions: image.DecodingOptions = {
927                sampleSize: 1,
928                editable: true,
929                desiredSize: { width: videoThumbWidth, height: videoThumbHeight },
930                desiredPixelFormat: image.PixelMapFormat.RGBA_8888
931              };
932              // TODO: 知识点: 使用回调函数中的ArrayBuffer数据生成小图,更新到小图列表中
933              imageSource.createPixelMap(decodingOptions).then((px: image.PixelMap) => {
934                let second = timeUs / CommonConstants.US_ONE_SECOND;
935                let framePos = count;
936                if (second === framePos) {
937                  logger.info(TAG,
938                    'framePos equal second, second=' + second + ' timeUs=' + timeUs + ' length=' + videoThumbs.length);
939                  videoThumbs[second].pixelMap = px;
940                  this.updateList(videoThumbs);
941                  count++;
942                  imageSource.release();
943                } else {
944                  logger.info(TAG, 'framePos not equal second, framePos=' + framePos + ' timeUs=' + timeUs);
945                  videoThumbs[framePos].pixelMap = px;
946                  this.updateList(videoThumbs);
947                  count++;
948                  imageSource.release();
949                }
950                // 获取到所需的图片数量后,停止获取
951                if (count == firstLoadMax) {
952                  this.videoTrimmerOption.loadFrameListener.onFinishLoad();
953                  this.isLoadFrame = false;
954                }
955              }).catch((err: Error) => {
956                // 部分视频创建图片异常,直接返回
957                logger.error(TAG, 'createPixelMap Failed, err = ' + err.name + ', message = ' + err.message);
958                this.videoTrimmerOption.loadFrameListener.onFinishLoad();
959                this.isLoadFrame = false;
960              })
961            }
962          }
963          // TODO: 知识点: 根据开始、结束时间,通过MP4Parser.getFrameAtTimeRang命令循环生成小图
964          let startTime = 0 * CommonConstants.US_ONE_SECOND + '';
965          let endTime = (firstLoadMax - 1) * CommonConstants.US_ONE_SECOND + '';
966          // 通过开始、结束时间,回调函数,获取视频小图
967          MP4Parser.getFrameAtTimeRang(startTime, endTime, MP4Parser.OPTION_CLOSEST, frameCallBack);
968        } else {
969          logger.info(TAG, 'setDataSource fail, error code =' + code.toString());
970        }
971      }
972    }
973    this.videoTrimmerOption.loadFrameListener.onStartLoad();
974    // 设置视频源和回调函数
975    MP4Parser.setDataSource(this.videoTrimmerOption.srcFilePath, callBack);
976    logger.info(TAG, 'initList list size=' + videoThumbs.length);
977    this.updateList(videoThumbs);
978  }
979
980  // 更新小图列表
981  updateList(videoThumbs: ThumbContent[]) {
982    this.mVideoThumbOption = {
983      videoThumbWidth: (this.mSeekBarWidth - 2 * this.thumbWidth) / this.maxCountRange,
984      videoThumbs: videoThumbs
985    }
986  }
987
988  // 初始视频选择区域范围
989  initRangeSeekBarView() {
990    this.mRangSeekBarOption = {
991      mLeftProgressPos: 0,
992      mRightProgressPos: this.mDuration,
993      // 最小时间 比如一屏幕10张图 间隔10s, 现在最小时间是3s,那么leftThumb和rightThumb的最小间隔是3张图宽度
994      mMinShootTime: this.videoMinTime,
995      // 最大时间 约束了右侧thumb和左侧thumb的距离
996      mMaxShootTime: this.videoMaxTime,
997      // 一屏幕展示最大图片数量, 一张图是1s,如果最大数量是20, 最大时间是10s,那么 rightThumb 距离 leftThumb 就是屏幕的10/20
998      // 需要保证mMaxCountRange <= mMaxShootTime 否则右侧的thumb会出界
999      mMaxCountRange: this.maxCountRange,
1000      mThumbWidth: this.thumbWidth,
1001      mPaddingTopBottom: this.paddingLineHeight
1002    };
1003  }
1004
1005  getRestoreState(): boolean {
1006    return this.isFromRestore;
1007  }
1008
1009  // 设置播放位置
1010  seekTo(msec: number) {
1011    if (this && this.avPlayer) {
1012      this.avPlayer?.seek(msec);
1013    }
1014  }
1015
1016  setRestoreState(fromRestore: boolean) {
1017    this.isFromRestore = fromRestore;
1018  }
1019
1020  // 获取选择时间范围后的提示信息
1021  getFormatString(res: Resource, instead: string): string {
1022    let resId = res.id;
1023    let ctx = this.videoTrimmerOption.context;
1024    if (ctx != undefined) {
1025    } else {
1026      ctx = getContext(this) as common.UIAbilityContext;
1027    }
1028
1029    let ret: string = '';
1030    let resMgr = ctx.resourceManager;
1031    try {
1032      let value = resMgr.getStringSync(resId);
1033      let values = value.split('%s');
1034      ret = values[0] + instead + values[1];
1035    } catch (error) {
1036      logger.info(TAG, 'VideTrimmerView getFormatString has error msg:' + (error as BusinessError).message);
1037    }
1038    logger.info(TAG, 'getFormatString success, ret = ' + ret);
1039    return ret;
1040  }
1041
1042  // TODO: 知识点: 视频剪辑。scaleCmd为视频压缩命令
1043  videoClip(sTime: string, eTime: string, srcFilePath: string, outFilePath: string, scaleCmd: string,
1044    callback: ICallBack) {
1045    let ffmpegCmd: string = '';
1046    if (scaleCmd !== '') {
1047      ffmpegCmd =
1048        'ffmpeg -y -i ' + srcFilePath + ' -vf ' + scaleCmd + ' -c:v mpeg4 -c:a aac -ss ' + sTime + ' -to ' + eTime +
1049          ' ' + outFilePath;
1050    } else {
1051      ffmpegCmd = 'ffmpeg -y -i ' + srcFilePath + ' -c:v mpeg4 -c:a aac -ss ' + sTime + ' -to ' + eTime +
1052        ' ' + outFilePath;
1053    }
1054    logger.info(TAG, 'videoClip cmd: ' + ffmpegCmd);
1055    MP4Parser.ffmpegCmd(ffmpegCmd, callback);
1056  }
1057}
1058
1059
1060
1061
1062
1063