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