ckplayer-plus 1.0.5 → 1.0.6

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/README.md CHANGED
@@ -77,7 +77,7 @@ React:
77
77
  | `preferBuiltinPlug` | `true` | 优先加载本包内置脚本,避免复用页面上其它全局 `Hls` |
78
78
  | `cdnFallback` | `true` | `getPath` 失败时回退 jsDelivr;内网无外网请设 `false` 并配 `pluginPath` |
79
79
  | `pluginPath` | 自动 | 插件脚本根路径。默认:`window.ckplayerBasePath` → 本包 `dist/` → jsDelivr CDN |
80
- | `seek` | `0` | 续播秒数(number 或数字字符串);会同步 `maxSeeTime`,兼容 `timeScheduleAdjust: 5` |
80
+ | `seek` | `0` | 续播秒数(number 或数字字符串);会同步 `maxSeeTime`。HLS 优先用 `startPosition`,并等待缓冲后再跳,降低非关键帧解码失败 |
81
81
  | `loaded` | — | **只支持函数**;字符串形式已禁用(防 XSS) |
82
82
 
83
83
  ### 进阶(按需)
package/dist/ckplayer.js CHANGED
@@ -141,7 +141,7 @@
141
141
  cdnFallback:true// getPath 失败时是否回退到 jsDelivr(内网可设 false,并配置 pluginPath)
142
142
  };
143
143
  // 打包时替换为真实版本;用于 bundler 下 getPath 失效时的 CDN 回退
144
- var ckplayerPlusDefaultCdn='https://cdn.jsdelivr.net/npm/ckplayer-plus@1.0.5/dist/';
144
+ var ckplayerPlusDefaultCdn='https://cdn.jsdelivr.net/npm/ckplayer-plus@1.0.6/dist/';
145
145
  function ckplayerEmbed(videoObj){
146
146
  /*
147
147
  * rightMenu
@@ -171,6 +171,11 @@
171
171
  var paused=true;//默认暂停状态
172
172
  var loadTime=0;//已加载部分
173
173
  var seekTime=0;//需要跳转的时间,初次播放以及切换清晰度后会用该变量记录需要seek的时间
174
+ var seekRetryTimer=null;//续播等待缓冲的重试计时器
175
+ var seekRetryCount=0;//续播重试次数
176
+ var seekApplyLock=false;//防止并发多次 currentTime 赋值
177
+ var decodeRecoverCount=0;//MEDIA_ERR_DECODE 恢复次数
178
+ var hlsStartPositionUsed=false;//是否已用 hls.js startPosition 处理续播
174
179
  var oldTime=0,playTime=0,firstSeekTime=-1,maxSeeTime=0;//oldTime=记录上次播放时间,playTime=当前播放时间,firstSeekTime=记录第一次拖动的时间,maxSeeTime=看过的最大时间
175
180
  var isChangeDef=true;//是否需要重置清晰度
176
181
  var playType='';//播放类别,默认='',是通过api接口播放,='button'则判定是通过点击按钮播放
@@ -409,7 +414,7 @@
409
414
  }
410
415
  var allowCdn=!vars || vars['cdnFallback']!==false;
411
416
  var cdn=(typeof window!=='undefined' && window.ckplayerPlusCdn) ? window.ckplayerPlusCdn : ckplayerPlusDefaultCdn;
412
- if(!allowCdn || !cdn || cdn.indexOf('1.0.5')>-1){
417
+ if(!allowCdn || !cdn || cdn.indexOf('1.0.6')>-1){
413
418
  // 避免占位版本或显式关闭 CDN 时请求坏地址;交给相对路径或业务 pluginPath
414
419
  return folder+'/'+file;
415
420
  }
@@ -842,11 +847,25 @@
842
847
  hlsPlayer=function (url){
843
848
  var startHls=function(){
844
849
  if (typeof Hls!=='undefined' && !isUndefined(Hls) && Hls.isSupported()) {
845
- hls = new Hls();
850
+ var hlsConf={};
851
+ // 断点续播:优先用 startPosition,让 hls 从正确分片/关键帧起播,避免裸 currentTime 解码失败
852
+ if(seekTime && seekTime>0 && (isUndefined(vars['live']) || !vars['live'])){
853
+ hlsConf.startPosition=seekTime;
854
+ maxSeeTime=Math.max(maxSeeTime,seekTime);
855
+ if(firstSeekTime<0){
856
+ firstSeekTime=seekTime;
857
+ }
858
+ seekTime=0;
859
+ hlsStartPositionUsed=true;
860
+ clearSeekRetry();
861
+ }
862
+ hls = new Hls(hlsConf);
846
863
  hls.loadSource(url);
847
864
  hls.attachMedia(video);
848
865
  hls.on(Hls.Events.MANIFEST_PARSED, function(){
849
- applyPendingSeek();
866
+ if(!hlsStartPositionUsed){
867
+ applyPendingSeek();
868
+ }
850
869
  });
851
870
  hls.on(Hls.Events.ERROR, function(event, data){
852
871
  var code=9;
@@ -865,6 +884,13 @@
865
884
  }
866
885
  }
867
886
  }
887
+ // 媒体解码类错误先尝试恢复,避免续播直接黑屏
888
+ if(fatal && data.type===Hls.ErrorTypes.MEDIA_ERROR && hls && typeof hls.recoverMediaError==='function'){
889
+ try{
890
+ hls.recoverMediaError();
891
+ fatal=false;
892
+ }catch(errRecover){}
893
+ }
868
894
  CT.error={code:code,message:message};
869
895
  eventTarget('error',CT.error);//注册监听error
870
896
  if(fatal){
@@ -1614,12 +1640,42 @@
1614
1640
  }
1615
1641
  }
1616
1642
  },
1643
+ /*
1644
+ * timeInBuffered
1645
+ * 功能:判断目标时间是否已在 buffered 区间内(续播前避免跳到未缓冲的非关键帧)
1646
+ */
1647
+ timeInBuffered=function(t){
1648
+ if(!video || isUndefined(video.buffered)){
1649
+ return false;
1650
+ }
1651
+ try{
1652
+ var buf=video.buffered;
1653
+ for(var i=0;i<buf.length;i++){
1654
+ if(t>=buf.start(i)-0.35 && t<=buf.end(i)+0.15){
1655
+ return true;
1656
+ }
1657
+ }
1658
+ }catch(event){}
1659
+ return false;
1660
+ },
1661
+ /*
1662
+ * clearSeekRetry
1663
+ * 功能:清理续播等待计时器
1664
+ */
1665
+ clearSeekRetry=function(){
1666
+ if(seekRetryTimer){
1667
+ clearTimeout(seekRetryTimer);
1668
+ seekRetryTimer=null;
1669
+ }
1670
+ seekRetryCount=0;
1671
+ },
1617
1672
  /*
1618
1673
  * applyPendingSeek
1619
- * 功能:在媒体就绪后应用挂起的 seekTime
1674
+ * 功能:在媒体就绪且目标时间可解码后再应用挂起的 seekTime
1675
+ * 避免断点续播过早 seek 触发 PIPELINE_ERROR_DECODE(非关键帧)
1620
1676
  */
1621
- applyPendingSeek=function(){
1622
- if(!seekTime || !video){
1677
+ applyPendingSeek=function(force){
1678
+ if(!seekTime || !video || seekApplyLock){
1623
1679
  return;
1624
1680
  }
1625
1681
  if(!isUndefined(vars['live']) && ((valType(vars['live'])=='boolean' && vars['live']) || valType(vars['live'])=='number')){
@@ -1630,19 +1686,93 @@
1630
1686
  if(dur && isFinite(dur) && dur>0 && t>=dur){
1631
1687
  t=Math.max(0,dur-0.3);
1632
1688
  }
1633
- // 续播水位同步(mode=5 依赖 maxSeeTime)
1689
+ if(t<=0){
1690
+ seekTime=0;
1691
+ clearSeekRetry();
1692
+ return;
1693
+ }
1694
+ var readyEnough=!isUndefined(video.readyState) && video.readyState>=2;
1695
+ var bufferedOk=timeInBuffered(t);
1696
+ // 未进入可播缓冲时先等待,并引导 hls 加载目标位置
1697
+ if(!force && !bufferedOk && (!readyEnough || video.readyState<3)){
1698
+ seekRetryCount++;
1699
+ if(seekRetryCount<=50){
1700
+ try{
1701
+ if(hls && typeof hls.startLoad==='function'){
1702
+ hls.startLoad(t);
1703
+ }
1704
+ }catch(event){}
1705
+ if(seekRetryTimer){
1706
+ clearTimeout(seekRetryTimer);
1707
+ }
1708
+ seekRetryTimer=setTimeout(function(){
1709
+ applyPendingSeek(false);
1710
+ },200);
1711
+ return;
1712
+ }
1713
+ }
1714
+ clearSeekRetry();
1634
1715
  if(t>0){
1635
1716
  maxSeeTime=Math.max(maxSeeTime,t);
1636
1717
  if(firstSeekTime<0){
1637
1718
  firstSeekTime=t;
1638
1719
  }
1639
1720
  }
1721
+ seekApplyLock=true;
1640
1722
  seekTime=0;
1641
1723
  try{
1642
1724
  video.currentTime=t;
1643
1725
  }catch(event){
1644
1726
  seekTime=t;
1727
+ seekApplyLock=false;
1728
+ }
1729
+ // seeked / 短暂延时后解锁,允许后续切换清晰度再 seek
1730
+ setTimeout(function(){
1731
+ seekApplyLock=false;
1732
+ },800);
1733
+ },
1734
+ /*
1735
+ * recoverDecodeError
1736
+ * 功能:断点续播触发的解码错误尝试恢复(Chrome PIPELINE_ERROR_DECODE)
1737
+ */
1738
+ recoverDecodeError=function(){
1739
+ if(decodeRecoverCount>=2){
1740
+ return false;
1741
+ }
1742
+ decodeRecoverCount++;
1743
+ var resumeAt=playTime>0?playTime:(vars['seek']?parseFloat(vars['seek']):0);
1744
+ if(isNaN(resumeAt) || resumeAt<0){
1745
+ resumeAt=0;
1645
1746
  }
1747
+ try{
1748
+ if(hls && typeof hls.recoverMediaError==='function'){
1749
+ hls.recoverMediaError();
1750
+ if(resumeAt>0){
1751
+ seekTime=resumeAt;
1752
+ seekApplyLock=false;
1753
+ setTimeout(function(){
1754
+ applyPendingSeek(true);
1755
+ },300);
1756
+ }
1757
+ return true;
1758
+ }
1759
+ }catch(event){}
1760
+ try{
1761
+ var soft=Math.max(0,resumeAt-1.5);
1762
+ seekTime=resumeAt;
1763
+ seekApplyLock=false;
1764
+ if(video){
1765
+ video.currentTime=soft;
1766
+ }
1767
+ setTimeout(function(){
1768
+ applyPendingSeek(true);
1769
+ if(video && video.paused && vars['autoplay']){
1770
+ try{video.play();}catch(e2){}
1771
+ }
1772
+ },400);
1773
+ return true;
1774
+ }catch(event){}
1775
+ return false;
1646
1776
  },
1647
1777
  /*
1648
1778
  * loadedMetaData
@@ -1834,6 +1964,12 @@
1834
1964
  if(!isUndefined(errorInfo.message)){
1835
1965
  msg=errorInfo.message;
1836
1966
  }
1967
+ // code=3 MEDIA_ERR_DECODE:续播非关键帧常见,先尝试恢复
1968
+ if(code===3 && recoverDecodeError()){
1969
+ CT.error={code:code,message:msg};
1970
+ eventTarget('error',CT.error);
1971
+ return;
1972
+ }
1837
1973
  CT.error={code:code,message:msg};
1838
1974
  showError();
1839
1975
  }
@@ -1887,6 +2023,8 @@
1887
2023
  eventTarget('error',CT.error);//注册监听error
1888
2024
  },
1889
2025
  seeked:function(){
2026
+ seekApplyLock=false;
2027
+ decodeRecoverCount=0;
1890
2028
  if(paused){
1891
2029
  player.play();
1892
2030
  }
@@ -2920,7 +3058,7 @@
2920
3058
  break;
2921
3059
  }
2922
3060
  pSliderMouseDown=false;
2923
- // 媒体未就绪时先记录,待 canplay/时长就绪后再应用
3061
+ // 媒体未就绪时只记录目标,禁止抢先 currentTime(会触发非关键帧解码失败)
2924
3062
  var ready=video.readyState>=2 || (duration && isFinite(duration) && duration>0);
2925
3063
  if(!ready){
2926
3064
  seekTime=target;
@@ -2930,7 +3068,6 @@
2930
3068
  firstSeekTime=target;
2931
3069
  }
2932
3070
  }
2933
- try{video.currentTime=target;}catch(event){}
2934
3071
  return player;
2935
3072
  }
2936
3073
  if(duration && isFinite(duration) && duration>0 && target>duration){
@@ -2943,6 +3080,12 @@
2943
3080
  if(vars['timeScheduleAdjust']==5 && target>maxSeeTime && seekTime && Math.abs(seekTime-target)<0.5){
2944
3081
  maxSeeTime=Math.max(maxSeeTime,target);
2945
3082
  }
3083
+ // 目标尚未进入缓冲时改为挂起,由 applyPendingSeek 等待后再跳
3084
+ if(target>0.5 && !timeInBuffered(target) && video.readyState<3){
3085
+ seekTime=target;
3086
+ applyPendingSeek(false);
3087
+ return player;
3088
+ }
2946
3089
  try{
2947
3090
  video.currentTime=target;
2948
3091
  seekTime=0;
@@ -6758,7 +6901,7 @@
6758
6901
  if(!isRetry && (!vars || vars['cdnFallback']!==false)){
6759
6902
  var cdn=(typeof window!=='undefined' && window.ckplayerPlusCdn) ? window.ckplayerPlusCdn : ckplayerPlusDefaultCdn;
6760
6903
  var m=String(file).match(/\/(hls\.js|flv\.js|mpegts\.js|language)\/([^/?#]+)(?:[?#].*)?$/i);
6761
- if(cdn && cdn.indexOf('1.0.5')===-1 && m){
6904
+ if(cdn && cdn.indexOf('1.0.6')===-1 && m){
6762
6905
  if(cdn.slice(-1)!=='/'){
6763
6906
  cdn+='/';
6764
6907
  }