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