@galacean/effects-plugin-multimedia 2.9.0-alpha.2 → 2.9.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3,11 +3,11 @@
3
3
  * Description: Galacean Effects player multimedia plugin
4
4
  * Author: Ant Group CO., Ltd.
5
5
  * Contributors: 云垣
6
- * Version: v2.9.0-alpha.2
6
+ * Version: v2.9.1-beta.0
7
7
  */
8
8
 
9
9
  import * as EFFECTS from '@galacean/effects';
10
- import { isFunction, spec, passRenderLevel, loadVideo, loadBinary, Plugin, effectsClass, math, assertExist, MaskableGraphic, Texture, RendererComponent, registerPlugin, logger } from '@galacean/effects';
10
+ import { isFunction, spec, passRenderLevel, loadVideo, loadBinary, Plugin, effectsClass, math, MaskableGraphic, Texture, RendererComponent, registerPlugin, logger } from '@galacean/effects';
11
11
 
12
12
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
13
13
  try {
@@ -557,25 +557,50 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
557
557
  var _this;
558
558
  _this = MaskableGraphic.call(this, engine) || this;
559
559
  /**
560
- * 播放标志位
561
- */ _this.played = false;
562
- _this.pendingPause = false;
563
- _this.threshold = 0.03;
564
- /**
565
- * 解决 video 暂停报错问题
566
- *
567
- * video.play(); // <-- This is asynchronous!
568
- *
569
- * video.pause();
570
- *
571
- * @see https://developer.chrome.com/blog/play-request-was-interrupted
572
- */ _this.isPlayLoading = false;
573
- /**
574
560
  * 视频元素是否激活
575
561
  */ _this.isVideoActive = false;
576
562
  /**
577
563
  * 是否为透明视频
578
564
  */ _this.transparent = false;
565
+ /**
566
+ * 是否由用户手动控制播放速率(覆盖合成的播放速率)
567
+ */ _this.manualPlaybackRate = false;
568
+ /**
569
+ * 是否由用户手动控制循环播放(覆盖合成的结束行为)
570
+ */ _this.manualLoop = false;
571
+ /**
572
+ * 是否由用户手动暂停视频
573
+ */ _this.manualPause = false;
574
+ /**
575
+ * 视频是否已开始播放
576
+ */ _this.playTriggered = false;
577
+ /**
578
+ * 上一次的视频时间,用于检测重播
579
+ */ _this.lastVideoTime = -1;
580
+ /**
581
+ * 视频是否已经销毁(用于 destroy 结束行为,确保只重置一次)
582
+ */ _this.videoDestroyed = false;
583
+ /**
584
+ * 视频是否处于 seek 中
585
+ * seek 期间禁止上传帧,避免 destroy 后 seek 回 0 期间渲染旧帧
586
+ */ _this.videoSeeking = false;
587
+ /**
588
+ * 待执行的 seek 目标时间,延迟到 onUpdate 中处理以避免竞态。
589
+ * 值为 null 表示没有待执行的 seek。
590
+ */ _this.pendingSeekTime = null;
591
+ /**
592
+ * 是否正在处理 gotoAndStop 的 seek
593
+ * 用于跳过 pause 事件触发的 pauseVideoElement,等 seek 完成后再暂停
594
+ */ _this.isGotoAndStopSeeking = false;
595
+ /**
596
+ * 是否刚收到 goto 事件,等待后续 play/pause 事件来区分场景
597
+ */ _this.isWaitingForGotoResult = false;
598
+ /**
599
+ * 当前正在执行的 play() Promise,用于串行化 play 调用,避免上的竞态
600
+ */ _this.playPromise = null;
601
+ /**
602
+ * 存储事件监听器的移除函数,用于销毁时清理
603
+ */ _this.eventDisposers = [];
579
604
  _this.name = "MVideo" + seed++;
580
605
  return _this;
581
606
  }
@@ -589,6 +614,11 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
589
614
  case 0:
590
615
  oldTexture = _this.renderer.texture;
591
616
  composition = _this.item.composition;
617
+ if (!composition) {
618
+ return [
619
+ 2
620
+ ];
621
+ }
592
622
  if (!(typeof input === "string")) return [
593
623
  3,
594
624
  2
@@ -607,11 +637,6 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
607
637
  texture = input;
608
638
  _state.label = 3;
609
639
  case 3:
610
- if (!composition) {
611
- return [
612
- 2
613
- ];
614
- }
615
640
  composition.textures.forEach(function(cachedTexture, index) {
616
641
  if (cachedTexture === oldTexture) {
617
642
  composition.textures[index] = texture;
@@ -635,21 +660,15 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
635
660
  if (!composition) {
636
661
  return;
637
662
  }
638
- this.handleGoto = function(option) {
639
- _this.setCurrentTime(_this.item.time);
640
- };
641
- this.handlePause = function() {
642
- _this.pauseVideo();
643
- };
644
- this.handlePlay = function(option) {
645
- if (_this.item.time < 0) {
646
- return;
647
- }
648
- _this.playVideo();
649
- };
650
- composition.on("goto", this.handleGoto);
651
- composition.on("pause", this.handlePause);
652
- composition.on("play", this.handlePlay);
663
+ this.eventDisposers.push(composition.on("goto", function() {
664
+ return _this.handleGoto();
665
+ }), composition.on("play", function() {
666
+ return _this.handleCompositionPlay();
667
+ }), composition.on("pause", function() {
668
+ return _this.handleCompositionPause();
669
+ }), composition.on("end", function() {
670
+ return _this.handleCompositionEnd();
671
+ }));
653
672
  };
654
673
  _proto.fromData = function fromData(data) {
655
674
  MaskableGraphic.prototype.fromData.call(this, data);
@@ -665,94 +684,504 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
665
684
  var videoAsset = this.engine.findObject(video);
666
685
  if (videoAsset) {
667
686
  this.video = videoAsset.data;
668
- this.setPlaybackRate(playbackRate);
687
+ this.video.playbackRate = playbackRate;
669
688
  this.setVolume(volume);
670
689
  this.setMuted(muted);
671
690
  var endBehavior = this.item.definition.endBehavior;
672
- // 如果元素设置为 destroy
673
691
  if (endBehavior === spec.EndBehavior.destroy || endBehavior === spec.EndBehavior.freeze) {
674
- this.setLoop(false);
692
+ this.video.loop = false;
675
693
  } else if (endBehavior === spec.EndBehavior.restart) {
676
- this.setLoop(true);
694
+ this.video.loop = true;
677
695
  }
678
696
  }
679
697
  }
680
698
  this.interaction = interaction;
681
- this.pauseVideo();
682
699
  if (this.transparent) {
683
700
  this.material.enableMacro("TRANSPARENT_VIDEO", this.transparent);
684
701
  }
685
702
  this.material.setColor("_Color", new math.Color().setFromArray(startColor));
686
703
  };
687
- _proto.render = function render(renderer) {
688
- MaskableGraphic.prototype.render.call(this, renderer);
689
- this.renderer.texture.uploadCurrentVideoFrame();
690
- };
691
704
  _proto.onUpdate = function onUpdate(dt) {
692
705
  MaskableGraphic.prototype.onUpdate.call(this, dt);
693
- var _this_item = this.item, videoTime = _this_item.time, videoDuration = _this_item.duration, videoEndBehavior = _this_item.endBehavior, composition = _this_item.composition;
694
- assertExist(composition);
695
- var _composition_rootItem = composition.rootItem, rootEndBehavior = _composition_rootItem.endBehavior, rootDuration = _composition_rootItem.duration;
696
- // 判断是否处于"结束状态":
697
- // - 视频时间为 0(未开始)
698
- // - 合成时间已达最大时长(播放完毕)
699
- // - 视频时间接近或等于其总时长(考虑容差阈值)
700
- var isEnd = videoTime === 0 || Math.abs(composition.time - rootDuration) <= this.threshold || Math.abs(videoTime - videoDuration) <= this.threshold;
701
- // 如果视频时间大于等于 0,且未到结束状态,并且尚未触发播放,则开始播放视频
702
- if (videoTime >= 0 && !isEnd && !this.played && this.isVideoActive) {
703
- this.playVideo();
704
- }
705
- // 当视频播放时间接近或超过其总时长时,根据其结束行为进行处理
706
- if (videoTime + this.threshold >= videoDuration) {
707
- if (videoEndBehavior === spec.EndBehavior.freeze) {
708
- var _this_video;
709
- if (!((_this_video = this.video) == null ? void 0 : _this_video.paused)) {
710
- this.pauseVideo();
711
- }
706
+ if (!this.video || this.video.readyState < 2) {
707
+ return;
708
+ }
709
+ // 处理延迟 seek(避免与 forwardTime 同步调用产生竞态)
710
+ if (this.processPendingSeek()) {
711
+ return;
712
+ }
713
+ // 检测当前是否为重播状态
714
+ this.detectCompositionRestart();
715
+ // 根据结束行为决定视频状态
716
+ if (this.shouldFreezeVideo()) {
717
+ this.freezeVideo();
718
+ return;
719
+ }
720
+ if (this.shouldStartVideo()) {
721
+ this.startVideo();
722
+ }
723
+ this.updatePlaybackRate();
724
+ this.ensureLoopFlag();
725
+ this.handleDestroyBehavior();
726
+ // 上传当前视频帧
727
+ if (!this.videoSeeking) {
728
+ this.renderer.texture.uploadCurrentVideoFrame();
729
+ }
730
+ };
731
+ _proto.onDestroy = function onDestroy() {
732
+ this.eventDisposers.forEach(function(dispose) {
733
+ return dispose();
734
+ });
735
+ this.eventDisposers = [];
736
+ MaskableGraphic.prototype.onDestroy.call(this);
737
+ this.playTriggered = false;
738
+ this.playPromise = null;
739
+ if (this.video) {
740
+ this.video.pause();
741
+ this.video.src = "";
742
+ this.video.load();
743
+ }
744
+ };
745
+ _proto.onDisable = function onDisable() {
746
+ MaskableGraphic.prototype.onDisable.call(this);
747
+ this.isVideoActive = false;
748
+ this.playTriggered = false;
749
+ this.pauseVideoElement();
750
+ };
751
+ _proto.onEnable = function onEnable() {
752
+ MaskableGraphic.prototype.onEnable.call(this);
753
+ this.isVideoActive = true;
754
+ this.playTriggered = false;
755
+ this.syncVideoToItemTime();
756
+ };
757
+ /**
758
+ * 处理 goto 事件:重置播放状态,记录待 seek 时间
759
+ */ _proto.handleGoto = function handleGoto() {
760
+ // 通过后续事件区分 gotoAndPlay 和 gotoAndStop
761
+ this.isWaitingForGotoResult = true;
762
+ this.playTriggered = false;
763
+ this.manualPause = false;
764
+ this.pendingSeekTime = this.getItemSeekTime();
765
+ };
766
+ /**
767
+ * 处理合成 pause 事件
768
+ * 如果刚收到 goto 事件且等待结果,说明是 gotoAndStop 场景
769
+ */ _proto.handleCompositionPause = function handleCompositionPause() {
770
+ // gotoAndStop 场景
771
+ if (this.isWaitingForGotoResult) {
772
+ this.isWaitingForGotoResult = false;
773
+ if (this.video && this.video.readyState >= 2) {
774
+ this.isGotoAndStopSeeking = true;
775
+ this.performSeek(this.getItemSeekTime(), false, true);
776
+ }
777
+ return;
778
+ }
779
+ // 普通 pause 事件:暂停视频
780
+ this.pauseVideoElement();
781
+ };
782
+ /**
783
+ * 处理合成 play 事件(合成开始/重播/恢复时触发)
784
+ */ _proto.handleCompositionPlay = function handleCompositionPlay() {
785
+ // 如果正在等待 goto 结果,说明是 gotoAndPlay 场景
786
+ if (this.isWaitingForGotoResult) {
787
+ this.isWaitingForGotoResult = false;
788
+ }
789
+ // 合成未结束时(暂停后恢复),恢复视频播放
790
+ if (!this.checkCompositionEnded()) {
791
+ var _this_video;
792
+ // 如果正在 seeking,不恢复播放,等 seek 完成后再恢复
793
+ if (this.canPlayCurrentItem() && ((_this_video = this.video) == null ? void 0 : _this_video.paused)) {
794
+ this.safePlay();
795
+ }
796
+ return;
797
+ }
798
+ this.playTriggered = false;
799
+ this.manualPause = false;
800
+ var videoEndBehavior = this.item.endBehavior;
801
+ if (videoEndBehavior === spec.EndBehavior.freeze && this.video) {
802
+ this.pendingSeekTime = 0;
803
+ } else if (videoEndBehavior === spec.EndBehavior.destroy) {
804
+ this.videoDestroyed = false;
805
+ }
806
+ };
807
+ /**
808
+ * 根据合成结束行为决定视频的后续处理
809
+ */ _proto.handleCompositionEnd = function handleCompositionEnd() {
810
+ var _this_item_composition;
811
+ var rootEndBehavior = (_this_item_composition = this.item.composition) == null ? void 0 : _this_item_composition.rootItem.endBehavior;
812
+ if (rootEndBehavior === spec.EndBehavior.restart) {
813
+ // 合成 restart:所有视频都需要 seek 回 0,和合成时间对齐
814
+ if (!this.videoSeeking) {
815
+ this.pendingSeekTime = 0;
816
+ }
817
+ this.playTriggered = false;
818
+ this.videoDestroyed = false;
819
+ return;
820
+ }
821
+ if (rootEndBehavior === spec.EndBehavior.forward) {
822
+ // forward:合成时间继续往前走,视频也继续播
823
+ return;
824
+ }
825
+ // freeze / destroy:合成真正结束,暂停视频
826
+ this.playTriggered = false;
827
+ this.pauseVideoElement();
828
+ };
829
+ /**
830
+ * 视频是否已播放到末尾
831
+ */ _proto.checkVideoEnded = function checkVideoEnded() {
832
+ var videoEndBehavior = this.item.endBehavior;
833
+ // restart 行为的视频永远不会"结束"(由浏览器原生 loop 处理)
834
+ if (videoEndBehavior === spec.EndBehavior.restart) {
835
+ return false;
836
+ }
837
+ return this.video.currentTime + VideoComponent.threshold >= this.video.duration;
838
+ };
839
+ /**
840
+ * 合成是否已到达结束时间
841
+ */ _proto.checkCompositionEnded = function checkCompositionEnded() {
842
+ var composition = this.item.composition;
843
+ if (!composition) {
844
+ return false;
845
+ }
846
+ return composition.time + VideoComponent.threshold >= composition.rootItem.duration;
847
+ };
848
+ /**
849
+ * 是否应该冻结视频(暂停在当前帧)
850
+ */ _proto.shouldFreezeVideo = function shouldFreezeVideo() {
851
+ var _this_item_composition;
852
+ var isVideoEnded = this.checkVideoEnded();
853
+ var isCompositionEnded = this.checkCompositionEnded();
854
+ var videoEndBehavior = this.item.endBehavior;
855
+ var rootEndBehavior = (_this_item_composition = this.item.composition) == null ? void 0 : _this_item_composition.rootItem.endBehavior;
856
+ // 合成结束且合成行为是 freeze 时冻结视频
857
+ var isCompositionFrozen = isCompositionEnded && rootEndBehavior === spec.EndBehavior.freeze;
858
+ // 合成结束且视频行为是 freeze,除合成 restart 外均冻结视频
859
+ var isCompositionEndedForVideo = rootEndBehavior !== spec.EndBehavior.restart && isCompositionEnded;
860
+ var isVideoFrozen = (isVideoEnded || isCompositionEndedForVideo) && videoEndBehavior === spec.EndBehavior.freeze;
861
+ return isVideoFrozen || isCompositionFrozen;
862
+ };
863
+ /**
864
+ * 是否应该启动视频播放
865
+ */ _proto.shouldStartVideo = function shouldStartVideo() {
866
+ if (this.playTriggered || !this.canPlayCurrentItem()) {
867
+ return false;
868
+ }
869
+ return true;
870
+ };
871
+ /**
872
+ * 处理延迟 seek,返回 true 表示本帧已处理 seek,应跳过后续逻辑
873
+ */ _proto.processPendingSeek = function processPendingSeek() {
874
+ if (this.pendingSeekTime === null) {
875
+ return false;
876
+ }
877
+ var seekTime = this.pendingSeekTime;
878
+ this.pendingSeekTime = null;
879
+ this.performSeek(seekTime);
880
+ return true;
881
+ };
882
+ /**
883
+ * 检测合成是否发生了 restart,并重置相关状态
884
+ */ _proto.detectCompositionRestart = function detectCompositionRestart() {
885
+ var videoTime = this.item.time;
886
+ if (this.lastVideoTime > 0 && videoTime < this.lastVideoTime) {
887
+ var _this_item_composition;
888
+ this.playTriggered = false;
889
+ this.videoDestroyed = false;
890
+ this.manualPause = false;
891
+ this.lastVideoTime = -1;
892
+ var rootEndBehavior = (_this_item_composition = this.item.composition) == null ? void 0 : _this_item_composition.rootItem.endBehavior;
893
+ var videoEndBehavior = this.item.endBehavior;
894
+ // 视频 restart 时,浏览器 loop 处理;合成 restart 时,前面函数已经 seek 回 0
895
+ if (rootEndBehavior !== spec.EndBehavior.restart && videoEndBehavior !== spec.EndBehavior.restart) {
896
+ this.pendingSeekTime = 0;
897
+ }
898
+ }
899
+ this.lastVideoTime = videoTime;
900
+ };
901
+ /**
902
+ * 冻结视频:停止播放,保持当前帧
903
+ */ _proto.freezeVideo = function freezeVideo() {
904
+ this.playTriggered = false;
905
+ if (!this.video.paused) {
906
+ this.pauseVideoElement();
907
+ }
908
+ };
909
+ /**
910
+ * 确保 restart 行为的视频设置了 loop 标志
911
+ * 手动模式下不自动设置,保持用户设置的值
912
+ */ _proto.ensureLoopFlag = function ensureLoopFlag() {
913
+ if (this.manualLoop) {
914
+ return;
915
+ }
916
+ if (this.item.endBehavior === spec.EndBehavior.restart && !this.video.loop) {
917
+ this.video.loop = true;
918
+ }
919
+ };
920
+ /**
921
+ * 处理 destroy 结束行为:视频播放到末尾后,seek 回 0 并清空纹理
922
+ * 确保合成 restart 时视频已在第 0 帧,不会闪最后一帧
923
+ */ _proto.handleDestroyBehavior = function handleDestroyBehavior() {
924
+ if (this.videoDestroyed || this.videoSeeking) {
925
+ return;
926
+ }
927
+ var isVideoEnded = this.checkVideoEnded();
928
+ if (isVideoEnded && this.item.endBehavior === spec.EndBehavior.destroy) {
929
+ this.videoDestroyed = true;
930
+ this.playTriggered = false;
931
+ this.performSeek(0, true);
932
+ }
933
+ };
934
+ /**
935
+ * 开始播放视频
936
+ */ _proto.startVideo = function startVideo() {
937
+ if (!this.video || this.playTriggered && !this.video.paused) {
938
+ return;
939
+ }
940
+ this.playTriggered = true;
941
+ this.safePlay();
942
+ };
943
+ /**
944
+ * 安全地调用 video.play(),串行化调用
945
+ */ _proto.safePlay = function safePlay() {
946
+ var _this = this;
947
+ if (!this.video) {
948
+ return;
949
+ }
950
+ // 已有 play() 在执行中,不重复调用,避免 AbortError
951
+ if (this.playPromise) {
952
+ return;
953
+ }
954
+ var promise = this.video.play();
955
+ this.playPromise = promise;
956
+ void promise.then(function() {
957
+ if (_this.playPromise === promise) {
958
+ _this.playPromise = null;
959
+ }
960
+ _this.updatePlaybackRate();
961
+ }).catch(function(error) {
962
+ if (_this.playPromise === promise) {
963
+ _this.playPromise = null;
712
964
  }
965
+ if (error.name === "AbortError") {
966
+ _this.playTriggered = false;
967
+ }
968
+ _this.engine.renderErrors.add(error);
969
+ });
970
+ };
971
+ /**
972
+ * 暂停底层视频元素
973
+ */ _proto.pauseVideoElement = function pauseVideoElement() {
974
+ var _this = this;
975
+ // gotoAndStop 场景:跳过暂停,等 seek 完成后再暂停
976
+ if (this.isGotoAndStopSeeking) {
977
+ return;
713
978
  }
714
- // 判断整个合成是否接近播放完成
715
- // composition.time + threshold >= rootDuration 表示即将结束
716
- if (composition.time + this.threshold >= rootDuration) {
717
- if (rootEndBehavior === spec.EndBehavior.freeze) {
718
- var _this_video1;
719
- if (!((_this_video1 = this.video) == null ? void 0 : _this_video1.paused)) {
720
- this.pauseVideo();
979
+ if (!this.video || this.video.paused) {
980
+ return;
981
+ }
982
+ this.video.pause();
983
+ if (this.playPromise) {
984
+ void this.playPromise.then(function() {
985
+ if (_this.video && !_this.video.paused) {
986
+ _this.video.pause();
987
+ }
988
+ });
989
+ }
990
+ };
991
+ /**
992
+ * seek 期间设置 videoSeeking=true,阻止 uploadCurrentVideoFrame 上传旧帧
993
+ * @param time 目标时间
994
+ * @param clearTexture 是否在 seek 期间清空纹理
995
+ * @param isGotoAndStop 是否为 gotoAndStop 场景
996
+ */ _proto.performSeek = function performSeek(time, clearTexture, isGotoAndStop) {
997
+ var _this = this;
998
+ if (clearTexture === void 0) clearTexture = false;
999
+ if (isGotoAndStop === void 0) isGotoAndStop = false;
1000
+ time = this.getClampedSeekTime(time);
1001
+ var wasPlaying = !this.video.paused;
1002
+ var isNoopSeek = function() {
1003
+ return !clearTexture && Math.abs(_this.video.currentTime - time) <= VideoComponent.threshold;
1004
+ };
1005
+ var finishNoopSeek = function() {
1006
+ _this.videoSeeking = false;
1007
+ _this.isGotoAndStopSeeking = false;
1008
+ if (isGotoAndStop) {
1009
+ _this.video.pause();
1010
+ } else if (wasPlaying && !_this.manualPause) {
1011
+ _this.safePlay();
1012
+ }
1013
+ };
1014
+ var doSeek = function() {
1015
+ if (isNoopSeek()) {
1016
+ finishNoopSeek();
1017
+ return;
1018
+ }
1019
+ _this.videoSeeking = true;
1020
+ if (clearTexture) {
1021
+ _this.material.setTexture("_MainTex", _this.engine.transparentTexture);
1022
+ }
1023
+ _this.video.addEventListener("seeked", function() {
1024
+ _this.videoSeeking = false;
1025
+ _this.isGotoAndStopSeeking = false;
1026
+ if (clearTexture) {
1027
+ _this.material.setTexture("_MainTex", _this.renderer.texture);
1028
+ }
1029
+ if (_this.video) {
1030
+ _this.renderer.texture.uploadCurrentVideoFrame();
1031
+ // gotoAndStop 场景:seek 完成后暂停
1032
+ if (isGotoAndStop) {
1033
+ _this.video.pause();
1034
+ } else if (wasPlaying && !_this.manualPause) {
1035
+ // 如果视频之前在播放(包括 goto 前在播放),seek 完成后恢复播放
1036
+ _this.safePlay();
1037
+ }
721
1038
  }
722
- } else if (rootEndBehavior === spec.EndBehavior.restart) {
723
- this.setCurrentTime(0);
1039
+ }, {
1040
+ once: true
1041
+ });
1042
+ _this.video.currentTime = time;
1043
+ };
1044
+ if (isGotoAndStop) {
1045
+ if (wasPlaying) {
1046
+ // 视频正在播放,直接 seek
1047
+ doSeek();
1048
+ } else if (isNoopSeek()) {
1049
+ finishNoopSeek();
1050
+ } else {
1051
+ // 视频暂停,先 play() 再 seek
1052
+ this.video.play().then(function() {
1053
+ doSeek();
1054
+ }).catch(function() {
1055
+ // play 失败时也尝试 seek
1056
+ doSeek();
1057
+ });
724
1058
  }
1059
+ } else {
1060
+ if (wasPlaying) {
1061
+ this.video.pause();
1062
+ }
1063
+ doSeek();
1064
+ }
1065
+ };
1066
+ /**
1067
+ * 更新视频播放速率
1068
+ * 手动模式下保持用户设置的速率不变,自动模式下根据 engine.speed * composition.speed 计算
1069
+ */ _proto.updatePlaybackRate = function updatePlaybackRate() {
1070
+ // 手动模式下不自动更新速率
1071
+ if (this.manualPlaybackRate) {
1072
+ return;
1073
+ }
1074
+ if (!this.video) {
1075
+ return;
1076
+ }
1077
+ var composition = this.item.composition;
1078
+ if (!composition) {
1079
+ return;
1080
+ }
1081
+ var playbackRate = this.engine.speed * composition.speed;
1082
+ if (this.video.playbackRate !== playbackRate) {
1083
+ this.video.playbackRate = playbackRate;
1084
+ }
1085
+ };
1086
+ /**
1087
+ * 当前 item 可播放的本地视频时间。
1088
+ * item.time 在 delay 前为负数,视频时间需要从 0 开始。
1089
+ */ _proto.getItemSeekTime = function getItemSeekTime() {
1090
+ return Math.max(0, this.getItemLocalTime());
1091
+ };
1092
+ /**
1093
+ * 获取 item 的本地时间。优先使用 timeline 写入的 item.time,
1094
+ * 未写入时使用合成时间和 item delay 做兜底。
1095
+ */ _proto.getItemLocalTime = function getItemLocalTime() {
1096
+ if (this.item.time >= 0) {
1097
+ return this.item.time;
1098
+ }
1099
+ var composition = this.item.composition;
1100
+ if (!composition) {
1101
+ return this.item.time;
1102
+ }
1103
+ var _this_item_definition_delay;
1104
+ return composition.time - ((_this_item_definition_delay = this.item.definition.delay) != null ? _this_item_definition_delay : 0);
1105
+ };
1106
+ /**
1107
+ * 将 seek 目标限制到视频有效时间范围内。
1108
+ */ _proto.getClampedSeekTime = function getClampedSeekTime(time) {
1109
+ var _this_video;
1110
+ var seekTime = Math.max(0, time);
1111
+ var duration = (_this_video = this.video) == null ? void 0 : _this_video.duration;
1112
+ if (!duration || !isFinite(duration)) {
1113
+ return seekTime;
725
1114
  }
1115
+ return Math.min(seekTime, duration);
1116
+ };
1117
+ /**
1118
+ * 组件重新启用时将视频时间对齐到 item 本地时间,但不直接触发播放。
1119
+ */ _proto.syncVideoToItemTime = function syncVideoToItemTime() {
1120
+ if (!this.video) {
1121
+ return;
1122
+ }
1123
+ var seekTime = this.getItemSeekTime();
1124
+ var clampedSeekTime = this.getClampedSeekTime(seekTime);
1125
+ if (Math.abs(this.video.currentTime - clampedSeekTime) > VideoComponent.threshold) {
1126
+ this.pendingSeekTime = clampedSeekTime;
1127
+ }
1128
+ };
1129
+ /**
1130
+ * 当前 item 已经进入自己的时间区间,且视频允许自动播放。
1131
+ */ _proto.canPlayCurrentItem = function canPlayCurrentItem() {
1132
+ if (!this.video || !this.isVideoActive || this.getItemLocalTime() < 0) {
1133
+ return false;
1134
+ }
1135
+ if (this.manualPause || this.videoDestroyed || this.videoSeeking || this.pendingSeekTime !== null) {
1136
+ return false;
1137
+ }
1138
+ return !this.checkVideoEnded();
726
1139
  };
727
1140
  /**
728
1141
  * 获取当前视频时长
729
- * @returns 视频时长
730
1142
  */ _proto.getDuration = function getDuration() {
731
1143
  return this.video ? this.video.duration : 0;
732
1144
  };
733
1145
  /**
734
1146
  * 获取当前视频播放时刻
735
- * @returns 当前视频播放时刻
736
1147
  */ _proto.getCurrentTime = function getCurrentTime() {
737
1148
  return this.video ? this.video.currentTime : 0;
738
1149
  };
739
1150
  /**
740
1151
  * 设置当前视频播放时刻
741
- * @param time 视频播放时刻
1152
+ * @param time 目标时间,会被限制在 [0, duration] 范围内
742
1153
  */ _proto.setCurrentTime = function setCurrentTime(time) {
743
- if (this.video) {
744
- this.video.currentTime = time;
1154
+ if (!this.video) {
1155
+ return;
1156
+ }
1157
+ var duration = this.video.duration;
1158
+ // 如果 duration 无效(如视频未加载),直接使用原值
1159
+ if (!duration || !isFinite(duration)) {
1160
+ this.pendingSeekTime = Math.max(0, time);
1161
+ return;
745
1162
  }
1163
+ this.pendingSeekTime = Math.max(0, Math.min(time, duration));
746
1164
  };
747
1165
  /**
748
- * 设置视频是否循环播放
1166
+ * 设置视频是否循环播放,调用后会覆盖合成的结束行为,改为由用户手动控制循环。
1167
+ * 调用 {@link resetLoop} 可恢复为由合成结束行为自动控制。
749
1168
  * @param loop 是否循环播放
750
1169
  */ _proto.setLoop = function setLoop(loop) {
1170
+ this.manualLoop = true;
751
1171
  if (this.video) {
752
1172
  this.video.loop = loop;
753
1173
  }
754
1174
  };
755
1175
  /**
1176
+ * 重置循环播放为合成自动控制模式
1177
+ */ _proto.resetLoop = function resetLoop() {
1178
+ this.manualLoop = false;
1179
+ // 将 video.loop 同步回合成应有的值,避免残留用户手动设置的状态
1180
+ if (this.video) {
1181
+ this.video.loop = this.item.endBehavior === spec.EndBehavior.restart;
1182
+ }
1183
+ };
1184
+ /**
756
1185
  * 设置视频是否静音
757
1186
  * @param muted 是否静音
758
1187
  */ _proto.setMuted = function setMuted(muted) {
@@ -783,115 +1212,37 @@ var VideoComponent = /*#__PURE__*/ function(MaskableGraphic) {
783
1212
  this.transparent = transparent;
784
1213
  };
785
1214
  /**
786
- * 设置视频播放速率
1215
+ * 设置视频播放速率,调用后会覆盖合成的速率,改为由用户手动控制速率。
1216
+ * 调用 {@link resetPlaybackRate} 可恢复为由合成速率自动控制。
787
1217
  * @param rate 视频播放速率
788
1218
  */ _proto.setPlaybackRate = function setPlaybackRate(rate) {
789
- if (!this.video || this.video.playbackRate === rate) {
790
- return;
1219
+ this.manualPlaybackRate = true;
1220
+ if (this.video) {
1221
+ this.video.playbackRate = rate;
791
1222
  }
792
- this.video.playbackRate = rate;
793
1223
  };
794
1224
  /**
795
- * 播放视频
1225
+ * 重置播放速率为合成自动控制模式
1226
+ */ _proto.resetPlaybackRate = function resetPlaybackRate() {
1227
+ this.manualPlaybackRate = false;
1228
+ };
1229
+ /**
1230
+ * 播放视频,同时取消手动暂停状态
796
1231
  * @since 2.3.0
797
1232
  */ _proto.playVideo = function playVideo() {
798
- var _this = this;
799
- if (this.played) {
800
- return;
801
- }
802
- if (this.video) {
803
- this.played = true;
804
- this.isPlayLoading = true;
805
- this.pendingPause = false;
806
- this.video.play().then(function() {
807
- _this.isPlayLoading = false;
808
- // 如果在 play pending 期间被请求了 pause,则立即暂停并复位 played
809
- if (!_this.played || _this.pendingPause) {
810
- var _this_video;
811
- _this.pendingPause = false;
812
- _this.played = false;
813
- (_this_video = _this.video) == null ? void 0 : _this_video.pause();
814
- }
815
- }).catch(function(error) {
816
- // 复位状态
817
- _this.isPlayLoading = false;
818
- _this.played = false;
819
- _this.pendingPause = false;
820
- if (error.name !== "AbortError") {
821
- _this.engine.renderErrors.add(error);
822
- }
823
- });
824
- }
1233
+ this.manualPause = false;
1234
+ this.startVideo();
825
1235
  };
826
1236
  /**
827
- * 暂停视频
1237
+ * 手动暂停视频
828
1238
  * @since 2.3.0
829
1239
  */ _proto.pauseVideo = function pauseVideo() {
830
- if (this.played) {
831
- this.played = false;
832
- }
833
- if (!this.video) {
834
- return;
835
- }
836
- if (this.isPlayLoading) {
837
- this.pendingPause = true;
838
- return;
839
- }
840
- this.video.pause();
841
- };
842
- _proto.onDestroy = function onDestroy() {
843
- var _this_item;
844
- MaskableGraphic.prototype.onDestroy.call(this);
845
- // 清理播放状态
846
- this.played = false;
847
- this.isPlayLoading = false;
848
- this.pendingPause = false;
849
- this.isVideoActive = false;
850
- // 清理video资源
851
- if (this.video) {
852
- // 暂停视频
853
- this.video.pause();
854
- // 移除video源,帮助垃圾回收
855
- this.video.removeAttribute("src");
856
- this.video.load();
857
- // 清理video引用
858
- this.video = undefined;
859
- }
860
- // 清理事件监听
861
- var composition = (_this_item = this.item) == null ? void 0 : _this_item.composition;
862
- if (composition) {
863
- if (this.handleGoto) {
864
- composition.off("goto", this.handleGoto);
865
- }
866
- if (this.handlePause) {
867
- composition.off("pause", this.handlePause);
868
- }
869
- if (this.handlePlay) {
870
- composition.off("play", this.handlePlay);
871
- }
872
- }
873
- // 清理处理器引用
874
- this.handleGoto = undefined;
875
- this.handlePause = undefined;
876
- this.handlePlay = undefined;
877
- };
878
- _proto.onDisable = function onDisable() {
879
- MaskableGraphic.prototype.onDisable.call(this);
880
- this.isVideoActive = false;
881
- this.pauseVideo();
882
- };
883
- _proto.onEnable = function onEnable() {
884
- MaskableGraphic.prototype.onEnable.call(this);
885
- this.isVideoActive = true;
886
- this.played = false;
887
- // 重播时确保视频同步到当前时间
888
- if (this.video && this.item.composition) {
889
- this.setCurrentTime(Math.max(0, this.item.time));
890
- }
891
- this.playVideo();
1240
+ this.manualPause = true;
1241
+ this.pauseVideoElement();
892
1242
  };
893
1243
  return VideoComponent;
894
1244
  }(MaskableGraphic);
1245
+ VideoComponent.threshold = 0.01;
895
1246
  VideoComponent = __decorate([
896
1247
  effectsClass(spec.DataType.VideoComponent)
897
1248
  ], VideoComponent);
@@ -1235,7 +1586,7 @@ AudioComponent = __decorate([
1235
1586
 
1236
1587
  /**
1237
1588
  * 插件版本号
1238
- */ var version = "2.9.0-alpha.2";
1589
+ */ var version = "2.9.1-beta.0";
1239
1590
  registerPlugin("video", VideoLoader);
1240
1591
  registerPlugin("audio", AudioLoader);
1241
1592
  logger.info("Plugin multimedia version: " + version + ".");