1import { p as prop, t as remove_input_defaults, d as bind_this, F as set_value, i as if_block, f as set_style, b as set_class, a as set_attribute } from './i18n-CGlVUOvE.js'; 2import { a as append, f as from_svg, ai as delegate, M as push$1, a8 as onMount, ad as user_effect, t as template_effect, O as pop, R as from_html, ab as state, ac as proxy, Z as get, $ as set, Q as child, T as reset, S as sibling, X as set_text, N as first_child, w as untrack, U as event, a2 as user_derived, a3 as comment } from './index-Bq2njFOY.js'; 3import { f as format_time, u as uploadToHuggingFace } from './utils.svelte-Bi9ASwv9.js'; 4import { I as IconButton } from './ScrollFade.svelte_svelte_type_style_lang-D0JPkGar.js'; 5import './MarkdownCode.svelte_svelte_type_style_lang-B29zDiob.js'; 6import { B as BlockLabel } from './BlockLabel-VpI11pHf.js'; 7import { D as DownloadLink } from './DownloadLink-D6EYxQxh.js'; 8import { E as Empty } from './Empty-BrRNpJ3n.js'; 9import { S as ShareButton } from './ShareButton-CORLYhd9.js'; 10import { D as Download } from './Download-D5ZNT7uE.js'; 11import { M as Music } from './Music-Dzd6j5YE.js'; 12import { I as IconButtonWrapper } from './IconButtonWrapper-DAsvkfJg.js'; 13import { g as get_skip_rewind_amount, u, p as process_audio, s as skip_audio, M as MinimalAudioPlayer } from './MinimalAudioPlayer-BNuZEaxr.js'; 14import { V as VolumeLevels, P as Pause, T as Trim } from './VolumeLevels-DLuAAZ7X.js'; 15import { P as Play } from './Play-CGGxKNeS.js'; 16import { U as Undo } from './Undo-4MCSCpCy.js'; 17 18var root$6 = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" fill="currentColor" stroke-width="1.5" viewBox="0 0 24 24" color="currentColor"><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M21.044 5.704a.6.6 0 0 1 .956.483v11.626a.6.6 0 0 1-.956.483l-7.889-5.813a.6.6 0 0 1 0-.966l7.89-5.813ZM10.044 5.704a.6.6 0 0 1 .956.483v11.626a.6.6 0 0 1-.956.483l-7.888-5.813a.6.6 0 0 1 0-.966l7.888-5.813Z"></path></svg>`); 19 20function Backward($$anchor) { 21 var svg = root$6(); 22 23 append($$anchor, svg); 24} 25 26var root$5 = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-closed-caption"><rect x="2" y="6" width="20" height="12" rx="2" ry="2"></rect><text x="12" y="15" font-family="sans-serif" font-size="8" font-weight="bold" fill="currentColor" stroke="none" text-anchor="middle">CC</text></svg>`); 27 28function ClosedCaption($$anchor) { 29 var svg = root$5(); 30 31 append($$anchor, svg); 32} 33 34var root$4 = from_svg(`<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" fill="currentColor" stroke-width="1.5" viewBox="0 0 24 24" color="currentColor"><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M2.956 5.704A.6.6 0 0 0 2 6.187v11.626a.6.6 0 0 0 .956.483l7.889-5.813a.6.6 0 0 0 0-.966l-7.89-5.813ZM13.956 5.704a.6.6 0 0 0-.956.483v11.626a.6.6 0 0 0 .956.483l7.889-5.813a.6.6 0 0 0 0-.966l-7.89-5.813Z"></path></svg>`); 35 36function Forward($$anchor) { 37 var svg = root$4(); 38 39 append($$anchor, svg); 40} 41 42class t{constructor(){this.listeners={};}on(t,e,i){if(this.listeners[t]||(this.listeners[t]=new Set),null==i?void 0:i.once){const i=(...n)=>{this.un(t,i),e(...n);};return this.listeners[t].add(i),()=>this.un(t,i)}return this.listeners[t].add(e),()=>this.un(t,e)}un(t,e){var i;null===(i=this.listeners[t])||void 0===i||i.delete(e);}once(t,e){return this.on(t,e,{once:true})}unAll(){this.listeners={};}emit(t,...e){this.listeners[t]&&this.listeners[t].forEach((t=>t(...e)));}}class e extends t{constructor(t){super(),this.subscriptions=[],this.isDestroyed=false,this.options=t;}onInit(){}_init(t){this.isDestroyed&&(this.subscriptions=[],this.isDestroyed=false),this.wavesurfer=t,this.onInit();}destroy(){this.emit("destroy"),this.subscriptions.forEach((t=>t())),this.subscriptions=[],this.isDestroyed=true,this.wavesurfer=void 0;}}function i(t,e,i,n,s=3,r=0,o=100){if(!t)return ()=>{};const h=matchMedia("(pointer: coarse)").matches;let a=()=>{};const l=l=>{if(l.button!==r)return;l.preventDefault(),l.stopPropagation();let d=l.clientX,c=l.clientY,u=false;const v=Date.now(),g=n=>{if(n.preventDefault(),n.stopPropagation(),h&&Date.now()-v<o)return;const r=n.clientX,a=n.clientY,l=r-d,g=a-c;if(u||Math.abs(l)>s||Math.abs(g)>s){const n=t.getBoundingClientRect(),{left:s,top:o}=n;u||(null==i||i(d-s,c-o),u=true),e(l,g,r-s,a-o),d=r,c=a;}},p=e=>{if(u){const i=e.clientX,s=e.clientY,r=t.getBoundingClientRect(),{left:o,top:h}=r;null==n||n(i-o,s-h);}a();},m=t=>{t.relatedTarget&&t.relatedTarget!==document.documentElement||p(t);},f=t=>{u&&(t.stopPropagation(),t.preventDefault());},b=t=>{u&&t.preventDefault();};document.addEventListener("pointermove",g),document.addEventListener("pointerup",p),document.addEventListener("pointerout",m),document.addEventListener("pointercancel",m),document.addEventListener("touchmove",b,{passive:false}),document.addEventListener("click",f,{capture:true}),a=()=>{document.removeEventListener("pointermove",g),document.removeEventListener("pointerup",p),document.removeEventListener("pointerout",m),document.removeEventListener("pointercancel",m),document.removeEventListener("touchmove",b),setTimeout((()=>{document.removeEventListener("click",f,{capture:true});}),10);};};return t.addEventListener("pointerdown",l),()=>{a(),t.removeEventListener("pointerdown",l);}}function n(t,e){const i=e.xmlns?document.createElementNS(e.xmlns,t):document.createElement(t);for(const[t,s]of Object.entries(e))if("children"===t&&s)for(const[t,e]of Object.entries(s))e instanceof Node?i.appendChild(e):"string"==typeof e?i.appendChild(document.createTextNode(e)):i.appendChild(n(t,e));else "style"===t?Object.assign(i.style,s):"textContent"===t?i.textContent=s:i.setAttribute(t,s.toString());return i}function s(t,e,i){const s=n(t,e||{});return null==i||i.appendChild(s),s}class r extends t{constructor(t,e,i=0){var n,s,r,o,h,a,l,d,c,u;super(),this.totalDuration=e,this.numberOfChannels=i,this.element=null,this.minLength=0,this.maxLength=1/0,this.contentEditable=false,this.subscriptions=[],this.updatingSide=void 0,this.isRemoved=false,this.subscriptions=[],this.id=t.id||`region-${Math.random().toString(32).slice(2)}`,this.start=this.clampPosition(t.start),this.end=this.clampPosition(null!==(n=t.end)&&void 0!==n?n:t.start),this.drag=null===(s=t.drag)||void 0===s||s,this.resize=null===(r=t.resize)||void 0===r||r,this.resizeStart=null===(o=t.resizeStart)||void 0===o||o,this.resizeEnd=null===(h=t.resizeEnd)||void 0===h||h,this.color=null!==(a=t.color)&&void 0!==a?a:"rgba(0, 0, 0, 0.1)",this.minLength=null!==(l=t.minLength)&&void 0!==l?l:this.minLength,this.maxLength=null!==(d=t.maxLength)&&void 0!==d?d:this.maxLength,this.channelIdx=null!==(c=t.channelIdx)&&void 0!==c?c:-1,this.contentEditable=null!==(u=t.contentEditable)&&void 0!==u?u:this.contentEditable,this.element=this.initElement(),this.setContent(t.content),this.setPart(),this.renderPosition(),this.initMouseEvents();}clampPosition(t){return Math.max(0,Math.min(this.totalDuration,t))}setPart(){var t;const e=this.start===this.end;null===(t=this.element)||void 0===t||t.setAttribute("part",`${e?"marker":"region"} ${this.id}`);}addResizeHandles(t){const e={position:"absolute",zIndex:"2",width:"6px",height:"100%",top:"0",cursor:"ew-resize",wordBreak:"keep-all"},n=s("div",{part:"region-handle region-handle-left",style:Object.assign(Object.assign({}
42,e),{left:"0",borderLeft:"2px solid rgba(0, 0, 0, 0.5)",borderRadius:"2px 0 0 2px"})},t),r=s("div",{part:"region-handle region-handle-right",style:Object.assign(Object.assign({},e),{right:"0",borderRight:"2px solid rgba(0, 0, 0, 0.5)",borderRadius:"0 2px 2px 0"})},t);this.subscriptions.push(i(n,(t=>this.onResize(t,"start")),(()=>null),(()=>this.onEndResizing("start")),1),i(r,(t=>this.onResize(t,"end")),(()=>null),(()=>this.onEndResizing("end")),1));}removeResizeHandles(t){const e=t.querySelector('[part*="region-handle-left"]'),i=t.querySelector('[part*="region-handle-right"]');e&&t.removeChild(e),i&&t.removeChild(i);}initElement(){if(this.isRemoved)return null;const t=this.start===this.end;let e=0,i=100;this.channelIdx>=0&&this.numberOfChannels>0&&this.channelIdx<this.numberOfChannels&&(i=100/this.numberOfChannels,e=i*this.channelIdx);const n=s("div",{style:{position:"absolute",top:`${e}%`,height:`${i}%`,backgroundColor:t?"none":this.color,borderLeft:t?"2px solid "+this.color:"none",borderRadius:"2px",boxSizing:"border-box",transition:"background-color 0.2s ease",cursor:this.drag?"grab":"default",pointerEvents:"all"}});return !t&&this.resize&&this.addResizeHandles(n),n}renderPosition(){if(!this.element)return;const t=this.start/this.totalDuration,e=(this.totalDuration-this.end)/this.totalDuration;this.element.style.left=100*t+"%",this.element.style.right=100*e+"%";}toggleCursor(t){var e;this.drag&&(null===(e=this.element)||void 0===e?void 0:e.style)&&(this.element.style.cursor=t?"grabbing":"grab");}initMouseEvents(){const{element:t}=this;t&&(t.addEventListener("click",(t=>this.emit("click",t))),t.addEventListener("mouseenter",(t=>this.emit("over",t))),t.addEventListener("mouseleave",(t=>this.emit("leave",t))),t.addEventListener("dblclick",(t=>this.emit("dblclick",t))),t.addEventListener("pointerdown",(()=>this.toggleCursor(true))),t.addEventListener("pointerup",(()=>this.toggleCursor(false))),this.subscriptions.push(i(t,(t=>this.onMove(t)),(()=>this.toggleCursor(true)),(()=>{this.toggleCursor(false),this.drag&&this.emit("update-end");}))),this.contentEditable&&this.content&&(this.contentClickListener=t=>this.onContentClick(t),this.contentBlurListener=()=>this.onContentBlur(),this.content.addEventListener("click",this.contentClickListener),this.content.addEventListener("blur",this.contentBlurListener)));}_onUpdate(t,e,i){var n;if(!(null===(n=this.element)||void 0===n?void 0:n.parentElement))return;const{width:s}=this.element.parentElement.getBoundingClientRect(),r=t/s*this.totalDuration;let o=e&&"start"!==e?this.start:this.start+r,h=e&&"end"!==e?this.end:this.end+r;const a=void 0!==i;a&&this.updatingSide&&this.updatingSide!==e&&("start"===this.updatingSide?o=i:h=i),o=Math.max(0,o),h=Math.min(this.totalDuration,h);const l=h-o;this.updatingSide=e;const d=l>=this.minLength&&l<=this.maxLength;o<=h&&(d||a)&&(this.start=o,this.end=h,this.renderPosition(),this.emit("update",e));}onMove(t){this.drag&&this._onUpdate(t);}onResize(t,e){this.resize&&(this.resizeStart||"start"!==e)&&(this.resizeEnd||"end"!==e)&&this._onUpdate(t,e);}
42onEndResizing(t){this.resize&&(this.emit("update-end",t),this.updatingSide=void 0);}onContentClick(t){t.stopPropagation();t.target.focus(),this.emit("click",t);}onContentBlur(){this.emit("update-end");}_setTotalDuration(t){this.totalDuration=t,this.renderPosition();}play(t){this.emit("play",t&&this.end!==this.start?this.end:void 0);}getContent(t=false){var e;return t?this.content||void 0:this.element instanceof HTMLElement?(null===(e=this.content)||void 0===e?void 0:e.innerHTML)||void 0:""}setContent(t){var e;if(this.element)if(this.content&&this.contentEditable&&(this.contentClickListener&&this.content.removeEventListener("click",this.contentClickListener),this.contentBlurListener&&this.content.removeEventListener("blur",this.contentBlurListener)),null===(e=this.content)||void 0===e||e.remove(),t){if("string"==typeof t){const e=this.start===this.end;this.content=s("div",{style:{padding:`0.2em ${e?.2:.4}em`,display:"inline-block"},textContent:t});}else this.content=t;this.contentEditable&&(this.content.contentEditable="true",this.contentClickListener=t=>this.onContentClick(t),this.contentBlurListener=()=>this.onContentBlur(),this.content.addEventListener("click",this.contentClickListener),this.content.addEventListener("blur",this.contentBlurListener)),this.content.setAttribute("part","region-content"),this.element.appendChild(this.content),this.emit("content-changed");}else this.content=void 0;}setOptions(t){var e,i;if(this.element){if(t.color&&(this.color=t.color,this.element.style.backgroundColor=this.color),void 0!==t.drag&&(this.drag=t.drag,this.element.style.cursor=this.drag?"grab":"default"),void 0!==t.start||void 0!==t.end){const n=this.start===this.end;this.start=this.clampPosition(null!==(e=t.start)&&void 0!==e?e:this.start),this.end=this.clampPosition(null!==(i=t.end)&&void 0!==i?i:n?this.start:this.end),this.renderPosition(),this.setPart();}if(t.content&&this.setContent(t.content),t.id&&(this.id=t.id,this.setPart()),void 0!==t.resize&&t.resize!==this.resize){const e=this.start===this.end;this.resize=t.resize,this.resize&&!e?this.addResizeHandles(this.element):this.removeResizeHandles(this.element);} void 0!==t.resizeStart&&(this.resizeStart=t.resizeStart),void 0!==t.resizeEnd&&(this.resizeEnd=t.resizeEnd);}}remove(){this.isRemoved=true,this.emit("remove"),this.subscriptions.forEach((t=>t())),this.element&&(this.element.remove(),this.element=null);}}class o extends e{constructor(t){super(t),this.regions=[],this.regionsContainer=this.initRegionsContainer();}static create(t){return new o(t)}onInit(){if(!this.wavesurfer)throw Error("WaveSurfer is not initialized");this.wavesurfer.getWrapper().appendChild(this.regionsContainer),this.subscriptions.push(this.wavesurfer.on("ready",(t=>{this.regions.forEach((e=>e._setTotalDuration(t)));})));let t=[];this.subscriptions.push(this.wavesurfer.on("timeupdate",(e=>{const i=this.regions.filter((t=>t.start<=e&&(t.end===t.start?t.start+.05:t.end)>=e));i.forEach((e=>{t.includes(e)||this.emit("region-in",e);})),t.forEach((t=>{i.includes(t)||this.emit("region-out",t);})),t=i;})));}initRegionsContainer(){return s("div",{part:"regions-container",style:{position:"absolute",top:"0",left:"0",width:"100%",height:"100%",zIndex:"5",pointerEvents:"none"}})}getRegions(){return this.regions}avoidOverlapping(t){t.content&&setTimeout((()=>{const e=t.content,i=e.getBoundingClientRect(),n=this.regions.map((e=>{if(e===t||!e.content)return 0;const n=e.content.getBoundingClientRect();return i.left<n.left+n.width&&n.left<i.left+i.width?n.height:0})).reduce(((t,e)=>t+e),0);e.style.marginTop=`${n}px`;}),10);}adjustScroll(t){var e,i;if(!t.element)return;const n=null===(i=null===(e=this.wavesurfer)||void 0===e?void 0:e.getWrapper())||void 0===i?void 0:i.parentElement;if(!n)return;const{clientWidth:s,scrollWidth:r}=n;if(r<=s)return;const o=n.getBoundingClientRect(),h=t.element.getBoundingClientRect(),a=h.left-o.left,l=h.right-o.left;a<0?n.scrollLeft+=a:l>s&&(n.scrollLeft+=l-s);}virtualAppend(t,e,i){const n=()=>
42{if(!this.wavesurfer)return;const n=this.wavesurfer.getWidth(),s=this.wavesurfer.getScroll(),r=e.clientWidth,o=this.wavesurfer.getDuration(),h=Math.round(t.start/o*r),a=h+(Math.round((t.end-t.start)/o*r)||1)>s&&h<s+n;a&&!i.parentElement?e.appendChild(i):!a&&i.parentElement&&i.remove();};setTimeout((()=>{if(!this.wavesurfer||!t.element)return;n();const e=this.wavesurfer.on("scroll",n),i=this.wavesurfer.on("zoom",n);this.subscriptions.push(e,i),t.once("remove",(()=>{e(),i();}));}),0);}saveRegion(t){if(!t.element)return;this.virtualAppend(t,this.regionsContainer,t.element),this.avoidOverlapping(t),this.regions.push(t);const e=[t.on("update",(e=>{e||this.adjustScroll(t),this.emit("region-update",t,e);})),t.on("update-end",(e=>{this.avoidOverlapping(t),this.emit("region-updated",t,e);})),t.on("play",(e=>{var i;null===(i=this.wavesurfer)||void 0===i||i.play(t.start,e);})),t.on("click",(e=>{this.emit("region-clicked",t,e);})),t.on("dblclick",(e=>{this.emit("region-double-clicked",t,e);})),t.on("content-changed",(()=>{this.emit("region-content-changed",t);})),t.once("remove",(()=>{e.forEach((t=>t())),this.regions=this.regions.filter((e=>e!==t)),this.emit("region-removed",t);}))];this.subscriptions.push(...e),this.emit("region-created",t);}addRegion(t){var e,i;if(!this.wavesurfer)throw Error("WaveSurfer is not initialized");const n=this.wavesurfer.getDuration(),s=null===(i=null===(e=this.wavesurfer)||void 0===e?void 0:e.getDecodedData())||void 0===i?void 0:i.numberOfChannels,o=new r(t,n,s);return this.emit("region-initialized",o),n?this.saveRegion(o):this.subscriptions.push(this.wavesurfer.once("ready",(t=>{o._setTotalDuration(t),this.saveRegion(o);}))),o}enableDragSelection(t,e=3){var n;const s=null===(n=this.wavesurfer)||void 0===n?void 0:n.getWrapper();if(!(s&&s instanceof HTMLElement))return ()=>{};let o=null,h=0,a=0;return i(s,((t,e,i)=>{o&&o._onUpdate(t,i>h?"end":"start",a);}),(e=>{var i,n;if(h=e,!this.wavesurfer)return;const s=this.wavesurfer.getDuration(),l=null===(n=null===(i=this.wavesurfer)||void 0===i?void 0:i.getDecodedData())||void 0===n?void 0:n.numberOfChannels,{width:d}=this.wavesurfer.getWrapper().getBoundingClientRect();a=h/d*s;const c=e/d*s,u=(e+5)/d*s;o=new r(Object.assign(Object.assign({},t),{start:c,end:u}),s,l),this.emit("region-initialized",o),o.element&&this.regionsContainer.appendChild(o.element);}),(()=>{o&&(this.saveRegion(o),o.updatingSide=void 0,o=null);}),e)}clearRegions(){this.regions.slice().forEach((t=>t.remove())),this.regions=[];}destroy(){this.clearRegions(),super.destroy(),this.regionsContainer.remove();}} 43 44var root$3 = from_html(`<input id="volume" class="volume-slider svelte-c0ird4" type="range" min="0" max="1" step="0.01"/>`); 45 46function VolumeControl($$anchor, $$props) { 47 push$1($$props, true); 48 49 let currentVolume = prop($$props, 'currentVolume', 15), 50 show_volume_slider = prop($$props, 'show_volume_slider', 15); 51 52 let volumeElement; 53 54 onMount(() => { 55 adjustSlider(); 56 }); 57 58 const adjustSlider = () => { 59 let slider = volumeElement; 60 61 if (!slider) return; 62 63 slider.style.background = `linear-gradient(to right, var(--color-accent) ${currentVolume() * 100}%, var(--neutral-400) ${currentVolume() * 100}%)`; 64 }; 65 66 user_effect(() => { 67 currentVolume(); 68 adjustSlider(); 69 }); 70 71 var input = root$3(); 72 73 remove_input_defaults(input); 74 input.__focusout = () => show_volume_slider(false); 75 76 input.__input = (e) => { 77 if (e.target instanceof HTMLInputElement) { 78 currentVolume(parseFloat(e.target.value)); 79 $$props.waveform?.setVolume(currentVolume()); 80 } 81 }; 82 83 bind_this(input, ($$value) => volumeElement = $$value, () => volumeElement); 84 template_effect(() => set_value(input, currentVolume())); 85 append($$anchor, input); 86 pop(); 87} 88 89delegate(['focusout', 'input']); 90 91var root_4$2 = from_html(`<button class="action icon cc-button svelte-72dh9g" data-testid="subtitles-toggle"><!></button>`); 92var root_6 = from_html(`<button class="action icon svelte-72dh9g" aria-label="Reset audio"><!></button>`); 93var root_7 = from_html(`<button class="action icon svelte-72dh9g" aria-label="Trim audio to selection"><!></button>`); 94var root_8 = from_html(`<button class="text-button svelte-72dh9g">Trim</button> <button class="text-button svelte-72dh9g">Cancel</button>`, 1); 95var root_5$1 = from_html(`<!> <!>`, 1); 96var root$2 = from_html(`<div class="controls svelte-72dh9g" data-testid="waveform-control
96s"><div class="control-wrapper svelte-72dh9g"><button class="action icon volume svelte-72dh9g" aria-label="Adjust volume"><!></button> <!> <button><span> </span></button></div> <div class="play-pause-wrapper svelte-72dh9g"><button class="rewind icon svelte-72dh9g"><!></button> <button class="play-pause-button icon svelte-72dh9g"><!></button> <button class="skip icon svelte-72dh9g"><!></button></div> <div class="settings-wrapper svelte-72dh9g"><!> <!></div></div>`); 97 98function WaveformControls($$anchor, $$props) { 99 push$1($$props, true); 100 101 let show_redo = prop($$props, 'show_redo', 3, false), 102 interactive = prop($$props, 'interactive', 3, false), 103 mode = prop($$props, 'mode', 15), 104 waveform_options = prop($$props, 'waveform_options', 19, () => ({})), 105 trim_region_settings = prop($$props, 'trim_region_settings', 19, () => ({})), 106 show_volume_slider = prop($$props, 'show_volume_slider', 15), 107 editable = prop($$props, 'editable', 3, true), 108 subtitles_toggle = prop($$props, 'subtitles_toggle', 15), 109 show_subtitles = prop($$props, 'show_subtitles', 3, false), 110 trimDuration = prop($$props, 'trimDuration', 15); 111 112 let playbackSpeeds = [0.5, 1, 1.5, 2]; 113 let playbackSpeed = state(proxy(playbackSpeeds[1])); 114 let trimRegion = state(null); 115 let activeRegion = state(null); 116 let leftRegionHandle = state(null); 117 let rightRegionHandle = state(null); 118 let activeHandle = state(""); 119 let currentVolume = state(1); 120 121 function ensureTrimRegion() { 122 if ($$props.container && $$props.waveform && !get(trimRegion)) { 123 set(trimRegion, $$props.waveform.registerPlugin(o.create()), true); 124 } 125 } 126 127 onMount(() => { 128 ensureTrimRegion(); 129 }); 130 131 user_effect(() => { 132 if (!get(trimRegion)) return; 133 134 const handler = (region) => { 135 region.play(); 136 }; 137 138 get(trimRegion).on("region-out", handler); 139 140 return () => { 141 get(trimRegion)?.un("region-out", handler); 142 }; 143 }); 144 145 user_effect(() => { 146 if (!get(trimRegion)) return; 147 148 const handler = (region) => { 149 trimDuration(region.end - region.start); 150 }; 151 152 get(trimRegion).on("region-updated", handler); 153 154 return () => { 155 get(trimRegion)?.un("region-updated", handler); 156 }; 157 }); 158 159 user_effect(() => { 160 if (!get(trimRegion)) return; 161 162 const handler = (region, e) => { 163 e.stopPropagation(); 164 set(activeRegion, region, true); 165 region.play(); 166 }; 167 168 get(trimRegion).on("region-clicked", handler); 169 170 return () => { 171 get(trimRegion)?.un("region-clicked", handler); 172 }; 173 }); 174 175 const addTrimRegion = () => { 176 if (!get(trimRegion)) return; 177 178 set( 179 activeRegion, 180 get(trimRegion)?.addRegion({ 181 start: $$props.audio_duration / 4, 182 end: $$props.audio_duration / 2, 183 ...trim_region_settings() 184 }), 185 true 186 ); 187 188 trimDuration(get(activeRegion).end - get(activeRegion).start); 189 }; 190 191 user_effect(() => { 192 if (get(activeRegion)) { 193 const shadowRoot = $$props.container.children[0].shadowRoot; 194 195 set(rightRegionHandle, shadowRoot.querySelector('[data-resize="right"]'), true); 196 set(leftRegionHandle, shadowRoot.querySelector('[data-resize="left"]'), true); 197 198 if (get(leftRegionHandle) && get(rightRegionHandle)) { 199 get(leftRegionHandle).setAttribute("role", "button"); 200 get(rightRegionHandle).setAttribute("role", "button"); 201 get(leftRegionHandle)?.setAttribute("aria-label", "Drag to adjust start time"); 202 get(rightRegionHandle)?.setAttribute("aria-label", "Drag to adjust end time"); 203 get(leftRegionHandle)?.setAttribute("tabindex", "0"); 204 get(rightRegionHandle)?.setAttribute("tabindex", "0"); 205 206 get(leftRegionHandle).addEventListener("focus", () => { 207 if (get(trimRegion)) set(activeHandle, "left"); 208 }); 209 210 get(rightRegionHandle).addEventListener("focus", () => { 211 if (get(trimRegion)) set(activeHandle, "right"); 212 }); 213 } 214 } 215 }); 216 217 const trimAudio = () => { 218 if ($$props.waveform && get(trimRegion)) { 219 if (get(activeRegion)) { 220 const start = get(activeRegion).start; 221 const end = get(activeRegion).end; 222 223 $$props.handle_trim_audio(start, end); 224 mode(""); 225 set(activeRegion, null); 226 } 227 } 228 }; 229 230 const clearRegions = () => {
231 get(trimRegion)?.getRegions().forEach((region) => { 232 region.remove(); 233 }); 234 235 get(trimRegion)?.clearRegions(); 236 }; 237 238 const toggleTrimmingMode = () => { 239 clearRegions(); 240 241 if (mode()) { 242 mode(""); 243 } else { 244 mode("edit"); 245 ensureTrimRegion(); 246 addTrimRegion(); 247 } 248 }; 249 250 const toggleSubtitles = () => { 251 subtitles_toggle(!subtitles_toggle()); 252 }; 253 254 const adjustRegionHandles = (handle, key) => { 255 let newStart; 256 let newEnd; 257 258 if (!get(activeRegion)) return; 259 260 if (handle === "left") { 261 if (key === "ArrowLeft") { 262 newStart = get(activeRegion).start - 0.05; 263 newEnd = get(activeRegion).end; 264 } else { 265 newStart = get(activeRegion).start + 0.05; 266 newEnd = get(activeRegion).end; 267 } 268 } else { 269 if (key === "ArrowLeft") { 270 newStart = get(activeRegion).start; 271 newEnd = get(activeRegion).end - 0.05; 272 } else { 273 newStart = get(activeRegion).start; 274 newEnd = get(activeRegion).end + 0.05; 275 } 276 } 277 278 get(activeRegion).setOptions({ start: newStart, end: newEnd }); 279 trimDuration(get(activeRegion).end - get(activeRegion).start); 280 }; 281 282 user_effect(() => { 283 if (get(trimRegion)) { 284 const handleKeydown = (e) => { 285 if (e.key === "ArrowLeft") { 286 adjustRegionHandles(get(activeHandle), "ArrowLeft"); 287 } else if (e.key === "ArrowRight") { 288 adjustRegionHandles(get(activeHandle), "ArrowRight"); 289 } 290 }; 291 292 window.addEventListener("keydown", handleKeydown); 293 294 return () => { 295 window.removeEventListener("keydown", handleKeydown); 296 }; 297 } 298 }); 299 300 var div = root$2(); 301 var div_1 = child(div); 302 var button = child(div_1); 303 304 button.__click = () => show_volume_slider(!show_volume_slider()); 305 306 let styles; 307 var node = child(button); 308 309 VolumeLevels(node, { 310 get currentVolume() { 311 return get(currentVolume); 312 } 313 }); 314 315 reset(button); 316 317 var node_1 = sibling(button, 2); 318 319 { 320 var consequent = ($$anchor) => { 321 VolumeControl($$anchor, { 322 get waveform() { 323 return $$props.waveform; 324 }, 325 326 get currentVolume() { 327 return get(currentVolume); 328 }, 329 330 set currentVolume($$value) { 331 set(currentVolume, $$value, true); 332 }, 333 334 get show_volume_slider() { 335 return show_volume_slider(); 336 }, 337 338 set show_volume_slider($$value) { 339 show_volume_slider($$value); 340 } 341 }); 342 }; 343 344 if_block(node_1, ($$render) => { 345 if (show_volume_slider()) $$render(consequent); 346 }); 347 } 348 349 var button_1 = sibling(node_1, 2); 350 let classes; 351 352 button_1.__click = () => { 353 set(playbackSpeed, playbackSpeeds[(playbackSpeeds.indexOf(get(playbackSpeed)) + 1) % playbackSpeeds.length], true); 354 $$props.waveform?.setPlaybackRate(get(playbackSpeed)); 355 }; 356 357 var span = child(button_1); 358 var text = child(span); 359 360 reset(span); 361 reset(button_1); 362 reset(div_1); 363 364 var div_2 = sibling(div_1, 2); 365 var button_2 = child(div_2); 366 367 button_2.__click = () => $$props.waveform?.skip(get_skip_rewind_amount($$props.audio_duration, waveform_options().skip_length) * -1); 368 369 var node_2 = child(button_2); 370 371 Backward(node_2); 372 reset(button_2); 373 374 var button_3 = sibling(button_2, 2); 375 376 button_3.__click = () => $$props.waveform?.playPause(); 377 378 var node_3 = child(button_3); 379 380 { 381 var consequent_1 = ($$anchor) => { 382 Pause($$anchor); 383 }; 384 385 var alternate = ($$anchor) => { 386 Play($$anchor); 387 }; 388 389 if_block(node_3, ($$render) => { 390 if ($$props.playing) $$render(consequent_1); else $$render(alternate, false); 391 }); 392 } 393 394 reset(button_3); 395 396 var button_4 = sibling(button_3, 2); 397 398 button_4.__click = () => $$props.waveform?.skip(get_skip_rewind_amount($$props.audio_duration, waveform_options().skip_length)); 399 400 var node_4 = child(button_4); 401 402 Forward(node_4); 403 reset(button_4); 404 reset(div_2); 405 406 var div_3 = sibling(div_2, 2); 407 var node_5 = child(div_3); 408 409 { 410 var consequent_2 = ($$anchor) => { 411 var button_5 = root_4$2(); 412 413 button_5.__click = toggleSubtitles; 414 415 var node_6 = child(button_5); 416 417 ClosedCaption(node_6); 418 reset(button_5); 419 template_effect(() => set_style(button_5, `color: ${subtitles_toggle() ? 'var(--color-accent)' : 'var(--neutral-400)'}`)); 420 append($$anchor, button_5); 421 }; 422 423 if_block(node_5, ($$render) => { 424 if (show_subtitles()) $$render(consequent_2); 425 }); 426 } 427 428 var node_7 = sibling(node_5, 2); 429 430 { 431 var consequent_5 = ($$anchor) => { 432 var fragment_3 = root_5$1(); 433 var node_8 = first_child(fragment_3); 434 435 { 436 var consequent_3 = ($$anchor) => { 437 var button_6 = root_6(); 438 439 button_6.__click = () => { 440 $$props.handle_reset_value(); 441 clearRegions(); 442 mode(""); 443 }; 444 445 var node_9 = child(button_6); 446 447 Undo(node_9); 448 reset(button_6); 449 append($$anchor, button_6); 450 }; 451 452 if_block(node_8, ($$render) => { 453 if (show_redo() && !mode()) $$render(consequent_3); 454 }); 455 } 456
457 var node_10 = sibling(node_8, 2); 458 459 { 460 var consequent_4 = ($$anchor) => { 461 var button_7 = root_7(); 462 463 button_7.__click = toggleTrimmingMode; 464 465 var node_11 = child(button_7); 466 467 Trim(node_11); 468 reset(button_7); 469 append($$anchor, button_7); 470 }; 471 472 var alternate_1 = ($$anchor) => { 473 var fragment_4 = root_8(); 474 var button_8 = first_child(fragment_4); 475 476 button_8.__click = trimAudio; 477 478 var button_9 = sibling(button_8, 2); 479 480 button_9.__click = toggleTrimmingMode; 481 append($$anchor, fragment_4); 482 }; 483 484 if_block(node_10, ($$render) => { 485 if (!mode()) $$render(consequent_4); else $$render(alternate_1, false); 486 }); 487 } 488 489 append($$anchor, fragment_3); 490 }; 491 492 if_block(node_7, ($$render) => { 493 if (editable() && interactive()) $$render(consequent_5); 494 }); 495 } 496 497 reset(div_3); 498 reset(div); 499 500 template_effect( 501 ($0, $1, $2, $3) => { 502 styles = set_style(button, '', styles, { 503 color: show_volume_slider() ? "var(--color-accent)" : "var(--neutral-400)" 504 }); 505 506 classes = set_class(button_1, 1, 'playback icon svelte-72dh9g', null, classes, { hidden: show_volume_slider() }); 507 set_attribute(button_1, 'aria-label', $0); 508 set_text(text, `${get(playbackSpeed) ?? ''}x`); 509 set_attribute(button_2, 'aria-label', $1); 510 set_attribute(button_3, 'aria-label', $2); 511 set_attribute(button_4, 'aria-label', `Skip forward by ${$3 ?? ''} seconds`); 512 }, 513 [ 514 () => `Adjust playback speed to ${playbackSpeeds[(playbackSpeeds.indexOf(get(playbackSpeed)) + 1) % playbackSpeeds.length]}x`, 515 () => `Skip backwards by ${get_skip_rewind_amount($$props.audio_duration, waveform_options().skip_length)} seconds`, 516 () => $$props.playing 517 ? $$props.i18n("audio.pause") 518 : $$props.i18n("audio.play"), 519 () => get_skip_rewind_amount($$props.audio_duration, waveform_options().skip_length) 520 ] 521 ); 522 523 append($$anchor, div); 524 pop(); 525} 526 527delegate(['click']); 528 529function getDefaultExportFromCjs (x) { 530 return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 531} 532 533var urlToolkit = {exports: {}}; 534 535(function (module, exports$1) { 536 // see https://tools.ietf.org/html/rfc1808 537 538 (function (root) { 539 var URL_REGEX = 540 /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/; 541 var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/; 542 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 543 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; 544 545 var URLToolkit = { 546 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 547 // E.g 548 // With opts.alwaysNormalize = false (default, spec compliant) 549 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 550 // With opts.alwaysNormalize = true (not spec compliant) 551 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 552 buildAbsoluteURL: function (baseURL, relativeURL, opts) { 553 opts = opts || {}; 554 // remove any remaining space and CRLF 555 baseURL = baseURL.trim(); 556 relativeURL = relativeURL.trim(); 557 if (!relativeURL) { 558 // 2a) If the embedded URL is entirely empty, it inherits the 559 // entire base URL (i.e., is set equal to the base URL) 560 // and we are done. 561 if (!opts.alwaysNormalize) { 562 return baseURL; 563 } 564 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 565 if (!basePartsForNormalise) { 566 throw new Error('Error trying to parse base URL.'); 567 } 568 basePartsForNormalise.path = URLToolkit.normalizePath( 569 basePartsForNormalise.path 570 ); 571 return URLToolkit.buildURLFromParts(basePartsForNormalise); 572 } 573 var relativeParts = URLToolkit.parseURL(relativeURL); 574 if (!relativeParts) { 575 throw new Error('Error trying to parse relative URL.'); 576 } 577 if (relativeParts.scheme) { 578 // 2b) If the embedded URL starts with a scheme name, it is 579 // interpreted as an absolute URL and we are done.
vendor: 2,415 bytes, lines 580-629
580 if (!opts.alwaysNormalize) { 581 return relativeURL; 582 } 583 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 584 return URLToolkit.buildURLFromParts(relativeParts); 585 } 586 var baseParts = URLToolkit.parseURL(baseURL); 587 if (!baseParts) { 588 throw new Error('Error trying to parse base URL.'); 589 } 590 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 591 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 592 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 593 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 594 baseParts.netLoc = pathParts[1]; 595 baseParts.path = pathParts[2]; 596 } 597 if (baseParts.netLoc && !baseParts.path) { 598 baseParts.path = '/'; 599 } 600 var builtParts = { 601 // 2c) Otherwise, the embedded URL inherits the scheme of 602 // the base URL. 603 scheme: baseParts.scheme, 604 netLoc: relativeParts.netLoc, 605 path: null, 606 params: relativeParts.params, 607 query: relativeParts.query, 608 fragment: relativeParts.fragment, 609 }; 610 if (!relativeParts.netLoc) { 611 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 612 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 613 // (if any) of the base URL. 614 builtParts.netLoc = baseParts.netLoc; 615 // 4) If the embedded URL path is preceded by a slash "/", the 616 // path is not relative and we skip to Step 7. 617 if (relativeParts.path[0] !== '/') { 618 if (!relativeParts.path) { 619 // 5) If the embedded URL path is empty (and not preceded by a 620 // slash), then the embedded URL inherits the base URL path 621 builtParts.path = baseParts.path; 622 // 5a) if the embedded URL's <params> is non-empty, we skip to 623 // step 7; otherwise, it inherits the <params> of the base 624 // URL (if any) and 625 if (!relativeParts.params) { 626 builtParts.params = baseParts.params; 627 // 5b) if the embedded URL's <query> is non-empty, we skip to 628 // step 7; otherwise, it inherits the <query> of the base 629 // URL (if any) and we skip to step 7.
630 if (!relativeParts.query) { 631 builtParts.query = baseParts.query; 632 } 633 } 634 } else { 635 // 6) The last segment of the base URL's path (anything 636 // following the rightmost slash "/", or the entire path if no 637 // slash is present) is removed and the embedded URL's path is 638 // appended in its place. 639 var baseURLPath = baseParts.path; 640 var newPath = 641 baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + 642 relativeParts.path; 643 builtParts.path = URLToolkit.normalizePath(newPath); 644 } 645 } 646 } 647 if (builtParts.path === null) { 648 builtParts.path = opts.alwaysNormalize 649 ? URLToolkit.normalizePath(relativeParts.path) 650 : relativeParts.path; 651 } 652 return URLToolkit.buildURLFromParts(builtParts); 653 }, 654 parseURL: function (url) { 655 var parts = URL_REGEX.exec(url); 656 if (!parts) { 657 return null; 658 } 659 return { 660 scheme: parts[1] || '', 661 netLoc: parts[2] || '', 662 path: parts[3] || '', 663 params: parts[4] || '', 664 query: parts[5] || '', 665 fragment: parts[6] || '', 666 }; 667 }, 668 normalizePath: function (path) { 669 // The following operations are 670 // then applied, in order, to the new path: 671 // 6a) All occurrences of "./", where "." is a complete path 672 // segment, are removed. 673 // 6b) If the path ends with "." as a complete path segment, 674 // that "." is removed. 675 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); 676 // 6c) All occurrences of "<segment>/../", where <segment> is a 677 // complete path segment not equal to "..", are removed. 678 // Removal of these path segments is performed iteratively, 679 // removing the leftmost matching pattern on each iteration, 680 // until no matching pattern remains. 681 // 6d) If the path ends with "<segment>/..", where <segment> is a 682 // complete path segment not equal to "..", that 683 // "<segment>/.." is removed. 684 while ( 685 path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length 686 ) {} 687 return path.split('').reverse().join(''); 688 }, 689 buildURLFromParts: function (parts) { 690 return ( 691 parts.scheme + 692 parts.netLoc + 693 parts.path + 694 parts.params + 695 parts.query + 696 parts.fragment 697 ); 698 }, 699 }; 700 701 module.exports = URLToolkit; 702 })(); 703} (urlToolkit)); 704 705var urlToolkitExports = urlToolkit.exports; 706 707function ownKeys(e, r) { 708 var t = Object.keys(e); 709 if (Object.getOwnPropertySymbols) { 710 var o = Object.getOwnPropertySymbols(e); 711 r && (o = o.filter(function (r) { 712 return Object.getOwnPropertyDescriptor(e, r).enumerable; 713 })), t.push.apply(t, o); 714 } 715 return t; 716} 717function _objectSpread2(e) { 718 for (var r = 1; r < arguments.length; r++) { 719 var t = null != arguments[r] ? arguments[r] : {}; 720 r % 2 ? ownKeys(Object(t), true).forEach(function (r) { 721 _defineProperty(e, r, t[r]); 722 }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { 723 Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); 724 }); 725 } 726 return e; 727} 728function _toPrimitive(t, r) { 729 if ("object" != typeof t || !t) return t; 730 var e = t[Symbol.toPrimitive]; 731 if (void 0 !== e) { 732 var i = e.call(t, r); 733 if ("object" != typeof i) return i; 734 throw new TypeError("@@toPrimitive must return a primitive value."); 735 } 736 return ("string" === r ? String : Number)(t); 737} 738function _toPropertyKey(t) { 739 var i = _toPrimitive(t, "string"); 740 return "symbol" == typeof i ? i : String(i); 741} 742function _defineProperty(obj, key, value) { 743 key = _toPropertyKey(key); 744 if (key in obj) { 745 Object.defineProperty(obj, key, { 746 value: value, 747 enumerable: true, 748 configurable: true, 749 writable: true 750 }); 751 } else { 752 obj[key] = value; 753 } 754 return obj; 755} 756function _extends() { 757 _extends = Object.assign ? Object.assign.bind() : function (target) { 758 for (var i = 1; i < arguments.length; i++) { 759 var source = arguments[i]; 760 for (var key in source) { 761 if (Object.prototype.hasOwnProperty.call(source, key)) { 762 target[key] = source[key]; 763 } 764 } 765 } 766 return target; 767 }; 768 return _extends.apply(this, arguments); 769} 770 771// https://caniuse.com/mdn-javascript_builtins_number_isfinite 772const isFiniteNumber = Number.isFinite || function (value) { 773 return typeof value === 'number' && isFinite(value); 774}; 775 776// https://caniuse.com/mdn-javascript_builtins_number_issafeinteger 777const isSafeInteger = Number.isSafeInteger || function (value) { 778 return typeof value === 'number' && Math.abs(value) <= MAX_SAFE_INTEGER; 779}; 780const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; 781 782let Events = /*#__PURE__*/function (Events) {
783 Events["MEDIA_ATTACHING"] = "hlsMediaAttaching"; 784 Events["MEDIA_ATTACHED"] = "hlsMediaAttached"; 785 Events["MEDIA_DETACHING"] = "hlsMediaDetaching"; 786 Events["MEDIA_DETACHED"] = "hlsMediaDetached"; 787 Events["BUFFER_RESET"] = "hlsBufferReset"; 788 Events["BUFFER_CODECS"] = "hlsBufferCodecs"; 789 Events["BUFFER_CREATED"] = "hlsBufferCreated"; 790 Events["BUFFER_APPENDING"] = "hlsBufferAppending"; 791 Events["BUFFER_APPENDED"] = "hlsBufferAppended"; 792 Events["BUFFER_EOS"] = "hlsBufferEos"; 793 Events["BUFFER_FLUSHING"] = "hlsBufferFlushing"; 794 Events["BUFFER_FLUSHED"] = "hlsBufferFlushed"; 795 Events["MANIFEST_LOADING"] = "hlsManifestLoading"; 796 Events["MANIFEST_LOADED"] = "hlsManifestLoaded"; 797 Events["MANIFEST_PARSED"] = "hlsManifestParsed"; 798 Events["LEVEL_SWITCHING"] = "hlsLevelSwitching"; 799 Events["LEVEL_SWITCHED"] = "hlsLevelSwitched"; 800 Events["LEVEL_LOADING"] = "hlsLevelLoading"; 801 Events["LEVEL_LOADED"] = "hlsLevelLoaded"; 802 Events["LEVEL_UPDATED"] = "hlsLevelUpdated"; 803 Events["LEVEL_PTS_UPDATED"] = "hlsLevelPtsUpdated"; 804 Events["LEVELS_UPDATED"] = "hlsLevelsUpdated"; 805 Events["AUDIO_TRACKS_UPDATED"] = "hlsAudioTracksUpdated"; 806 Events["AUDIO_TRACK_SWITCHING"] = "hlsAudioTrackSwitching"; 807 Events["AUDIO_TRACK_SWITCHED"] = "hlsAudioTrackSwitched"; 808 Events["AUDIO_TRACK_LOADING"] = "hlsAudioTrackLoading"; 809 Events["AUDIO_TRACK_LOADED"] = "hlsAudioTrackLoaded"; 810 Events["SUBTITLE_TRACKS_UPDATED"] = "hlsSubtitleTracksUpdated"; 811 Events["SUBTITLE_TRACKS_CLEARED"] = "hlsSubtitleTracksCleared"; 812 Events["SUBTITLE_TRACK_SWITCH"] = "hlsSubtitleTrackSwitch"; 813 Events["SUBTITLE_TRACK_LOADING"] = "hlsSubtitleTrackLoading"; 814 Events["SUBTITLE_TRACK_LOADED"] = "hlsSubtitleTrackLoaded"; 815 Events["SUBTITLE_FRAG_PROCESSED"] = "hlsSubtitleFragProcessed"; 816 Events["CUES_PARSED"] = "hlsCuesParsed"; 817 Events["NON_NATIVE_TEXT_TRACKS_FOUND"] = "hlsNonNativeTextTracksFound"; 818 Events["INIT_PTS_FOUND"] = "hlsInitPtsFound"; 819 Events["FRAG_LOADING"] = "hlsFragLoading"; 820 Events["FRAG_LOAD_EMERGENCY_ABORTED"] = "hlsFragLoadEmergencyAborted"; 821 Events["FRAG_LOADED"] = "hlsFragLoaded"; 822 Events["FRAG_DECRYPTED"] = "hlsFragDecrypted"; 823 Events["FRAG_PARSING_INIT_SEGMENT"] = "hlsFragParsingInitSegment"; 824 Events["FRAG_PARSING_USERDATA"] = "hlsFragParsingUserdata"; 825 Events["FRAG_PARSING_METADATA"] = "hlsFragParsingMetadata"; 826 Events["FRAG_PARSED"] = "hlsFragParsed"; 827 Events["FRAG_BUFFERED"] = "hlsFragBuffered"; 828 Events["FRAG_CHANGED"] = "hlsFragChanged"; 829 Events["FPS_DROP"] = "hlsFpsDrop"; 830 Events["FPS_DROP_LEVEL_CAPPING"] = "hlsFpsDropLevelCapping"; 831 Events["MAX_AUTO_LEVEL_UPDATED"] = "hlsMaxAutoLevelUpdated"; 832 Events["ERROR"] = "hlsError"; 833 Events["DESTROYING"] = "hlsDestroying"; 834 Events["KEY_LOADING"] = "hlsKeyLoading"; 835 Events["KEY_LOADED"] = "hlsKeyLoaded"; 836 Events["LIVE_BACK_BUFFER_REACHED"] = "hlsLiveBackBufferReached"; 837 Events["BACK_BUFFER_REACHED"] = "hlsBackBufferReached"; 838 Events["STEERING_MANIFEST_LOADED"] = "hlsSteeringManifestLoaded"; 839 return Events; 840}({}); 841 842/** 843 * Defines each Event type and payload by Event name. Used in {@link hls.js#HlsEventEmitter} to strongly type the event listener API. 844 */ 845 846let ErrorTypes = /*#__PURE__*/function (ErrorTypes) { 847 ErrorTypes["NETWORK_ERROR"] = "networkError"; 848 ErrorTypes["MEDIA_ERROR"] = "mediaError"; 849 ErrorTypes["KEY_SYSTEM_ERROR"] = "keySystemError"; 850 ErrorTypes["MUX_ERROR"] = "muxError"; 851 ErrorTypes["OTHER_ERROR"] = "otherError"; 852 return ErrorTypes; 853}({}); 854let ErrorDetails = /*#__PURE__*/function (ErrorDetails) { 855 ErrorDetails["KEY_SYSTEM_NO_KEYS"] = "keySystemNoKeys"; 856 ErrorDetails["KEY_SYSTEM_NO_ACCESS"] = "keySystemNoAccess"; 857 ErrorDetails["KEY_SYSTEM_NO_SESSION"] = "keySystemNoSession"; 858 ErrorDetails["KEY_SYSTEM_NO_CONFIGURED_LICENSE"] = "keySystemNoConfiguredLicense"; 859 ErrorDetails["KEY_SYSTEM_LICENSE_REQUEST_FAILED"] = "keySystemLicenseRequestFailed"; 860 ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED"] = "keySystemServerCertificateRequestFailed"; 861 ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED"] = "keySystemServerCertificateUpdateFailed"; 862 ErrorDetails["KEY_SYSTEM_SESSION_UPDATE_FAILED"] = "keySystemSessionUpdateFailed"; 863 ErrorDetails["KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED"] = "keySystemStatusOutputRestricted"; 864 ErrorDetails["KEY_SYSTEM_STATUS_INTERNAL_ERROR"] = "keySystemStatusInternalError"; 865 ErrorDetails["MANIFEST_LOAD_ERROR"] = "manifestLoadError"; 866 ErrorDetails["MANIFEST_LOAD_TIMEOUT"] = "manifestLoadTimeOut"; 867 ErrorDetails["MANIFEST_PARSING_ERROR"] = "manifestParsingError"; 868 ErrorDetails["MANIFEST_INCOMPATIBLE_CODECS_ERROR"] = "manifestIncompatibleCodecsError"; 869 ErrorDetails["LEVEL_EMPTY_ERROR"] = "levelEmptyError"; 870 ErrorDetails["LEVEL_LOAD_ERROR"] = "levelLoadError"; 871 ErrorDetails["LEVEL_LOAD_TIMEOUT"] = "levelLoadTimeOut"; 872 ErrorDetails["LEVEL_PARSING_ERROR"] = "levelParsingError"; 873 ErrorDetails["LEVEL_SWITCH_ERROR"] = "levelSwitchError"; 874 ErrorDetails["AUDIO_TRACK_LOAD_ERROR"] = "audioTrackLoadError"; 875 ErrorDetails["AUDIO_TRACK_LOAD_TIMEOUT"] = "audioTrackLoadTimeOut"; 876 ErrorDetails["SUBTITLE_LOAD_ERROR"] = "subtitleTrackLoadError"; 877 ErrorDetails["SUBTITLE_TRACK_LOAD_TIMEOUT"] = "subtitleTrackLoadTimeOut"; 878 ErrorDetails["FRAG_LOAD_ERROR"] = "fragLoadError"; 879 ErrorDetails["FRAG_LOAD_TIMEOUT"] = "fragLoadTimeOut"; 880 ErrorDetails["FRAG_DECRYPT_ERROR"] = "fragDecryptError"; 881 ErrorDetails["FRAG_PARSING_ERROR"] = "fragParsingError"; 882 ErrorDetails["FRAG_GAP"] = "fragGap";
883 ErrorDetails["REMUX_ALLOC_ERROR"] = "remuxAllocError"; 884 ErrorDetails["KEY_LOAD_ERROR"] = "keyLoadError"; 885 ErrorDetails["KEY_LOAD_TIMEOUT"] = "keyLoadTimeOut"; 886 ErrorDetails["BUFFER_ADD_CODEC_ERROR"] = "bufferAddCodecError"; 887 ErrorDetails["BUFFER_INCOMPATIBLE_CODECS_ERROR"] = "bufferIncompatibleCodecsError"; 888 ErrorDetails["BUFFER_APPEND_ERROR"] = "bufferAppendError"; 889 ErrorDetails["BUFFER_APPENDING_ERROR"] = "bufferAppendingError"; 890 ErrorDetails["BUFFER_STALLED_ERROR"] = "bufferStalledError"; 891 ErrorDetails["BUFFER_FULL_ERROR"] = "bufferFullError"; 892 ErrorDetails["BUFFER_SEEK_OVER_HOLE"] = "bufferSeekOverHole"; 893 ErrorDetails["BUFFER_NUDGE_ON_STALL"] = "bufferNudgeOnStall"; 894 ErrorDetails["INTERNAL_EXCEPTION"] = "internalException"; 895 ErrorDetails["INTERNAL_ABORTED"] = "aborted"; 896 ErrorDetails["UNKNOWN"] = "unknown"; 897 return ErrorDetails; 898}({}); 899 900const noop = function noop() {}; 901const fakeLogger = { 902 trace: noop, 903 debug: noop, 904 log: noop, 905 warn: noop, 906 info: noop, 907 error: noop 908}; 909let exportedLogger = fakeLogger; 910 911// let lastCallTime; 912// function formatMsgWithTimeInfo(type, msg) { 913// const now = Date.now(); 914// const diff = lastCallTime ? '+' + (now - lastCallTime) : '0'; 915// lastCallTime = now; 916// msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )'; 917// return msg; 918// } 919 920function consolePrintFn(type) { 921 const func = self.console[type]; 922 if (func) { 923 return func.bind(self.console, `[${type}] >`); 924 } 925 return noop; 926} 927function exportLoggerFunctions(debugConfig, ...functions) { 928 functions.forEach(function (type) { 929 exportedLogger[type] = debugConfig[type] ? debugConfig[type].bind(debugConfig) : consolePrintFn(type); 930 }); 931} 932function enableLogs(debugConfig, id) { 933 // check that console is available 934 if (typeof console === 'object' && debugConfig === true || typeof debugConfig === 'object') { 935 exportLoggerFunctions(debugConfig, 936 // Remove out from list here to hard-disable a log-level 937 // 'trace', 938 'debug', 'log', 'info', 'warn', 'error'); 939 // Some browsers don't allow to use bind on console object anyway 940 // fallback to default if needed 941 try { 942 exportedLogger.log(`Debug logs enabled for "${id}" in hls.js version ${"1.5.13"}`); 943 } catch (e) { 944 exportedLogger = fakeLogger; 945 } 946 } else { 947 exportedLogger = fakeLogger; 948 } 949} 950const logger = exportedLogger; 951 952const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/; 953const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g; 954 955// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js 956class AttrList { 957 constructor(attrs) { 958 if (typeof attrs === 'string') { 959 attrs = AttrList.parseAttrList(attrs); 960 } 961 _extends(this, attrs); 962 } 963 get clientAttrs() { 964 return Object.keys(this).filter(attr => attr.substring(0, 2) === 'X-'); 965 } 966 decimalInteger(attrName) { 967 const intValue = parseInt(this[attrName], 10); 968 if (intValue > Number.MAX_SAFE_INTEGER) { 969 return Infinity; 970 } 971 return intValue; 972 } 973 hexadecimalInteger(attrName) { 974 if (this[attrName]) { 975 let stringValue = (this[attrName] || '0x').slice(2); 976 stringValue = (stringValue.length & 1 ? '0' : '') + stringValue; 977 const value = new Uint8Array(stringValue.length / 2); 978 for (let i = 0; i < stringValue.length / 2; i++) { 979 value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16); 980 } 981 return value; 982 } else { 983 return null; 984 } 985 } 986 hexadecimalIntegerAsNumber(attrName) { 987 const intValue = parseInt(this[attrName], 16); 988 if (intValue > Number.MAX_SAFE_INTEGER) { 989 return Infinity; 990 } 991 return intValue; 992 } 993 decimalFloatingPoint(attrName) { 994 return parseFloat(this[attrName]); 995 } 996 optionalFloat(attrName, defaultValue) { 997 const value = this[attrName]; 998 return value ? parseFloat(value) : defaultValue; 999 } 1000 enumeratedString(attrName) { 1001 return this[attrName]; 1002 } 1003 bool(attrName) { 1004 return this[attrName] === 'YES'; 1005 } 1006 decimalResolution(attrName) { 1007 const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]); 1008 if (res === null) { 1009 return undefined; 1010 } 1011 return { 1012 width: parseInt(res[1], 10), 1013 height: parseInt(res[2], 10) 1014 }; 1015 } 1016 static parseAttrList(input) { 1017 let match; 1018 const attrs = {}; 1019 const quote = '"'; 1020 ATTR_LIST_REGEX.lastIndex = 0; 1021 while ((match = ATTR_LIST_REGEX.exec(input)) !== null) { 1022 let value = match[2]; 1023 if (value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1) { 1024 value = value.slice(1, -1); 1025 } 1026 const name = match[1].trim(); 1027 attrs[name] = value; 1028 } 1029 return attrs; 1030 } 1031} 1032 1033// Avoid exporting const enum so that these values can be inlined 1034 1035function isDateRangeCueAttribute(attrName) { 1036 return attrName !== "ID" && attrName !== "CLASS" && attrName !== "START-DATE" && attrName !== "DURATION" && attrName !== "END-DATE" && attrName !== "END-ON-NEXT"; 1037} 1038function isSCTE35Attribute(attrName) { 1039 return attrName === "SCTE35-OUT" || attrName === "SCTE35-IN"; 1040} 1041class DateRange { 1042 constructor(dateRangeAttr, dateRangeWithSameId) { 1043 this.attr = void 0; 1044 this._startDate = void 0; 1045 this._endDate = void 0; 1046 this._badValueForSameId = void 0; 1047 if (dateRangeWithSameId) { 1048 const previousAttr = dateRangeWithSameId.attr; 1049 for (const key in previousAttr) { 1050 if (Object.prototype.hasOwnProperty.call(dateRangeAttr, key) && dateRangeAttr[key] !== previousAttr[key]) {
1051 logger.warn(`DATERANGE tag attribute: "${key}" does not match for tags with ID: "${dateRangeAttr.ID}"`); 1052 this._badValueForSameId = key; 1053 break; 1054 } 1055 } 1056 // Merge DateRange tags with the same ID 1057 dateRangeAttr = _extends(new AttrList({}), previousAttr, dateRangeAttr); 1058 } 1059 this.attr = dateRangeAttr; 1060 this._startDate = new Date(dateRangeAttr["START-DATE"]); 1061 if ("END-DATE" in this.attr) { 1062 const endDate = new Date(this.attr["END-DATE"]); 1063 if (isFiniteNumber(endDate.getTime())) { 1064 this._endDate = endDate; 1065 } 1066 } 1067 } 1068 get id() { 1069 return this.attr.ID; 1070 } 1071 get class() { 1072 return this.attr.CLASS; 1073 } 1074 get startDate() { 1075 return this._startDate; 1076 } 1077 get endDate() { 1078 if (this._endDate) { 1079 return this._endDate; 1080 } 1081 const duration = this.duration; 1082 if (duration !== null) { 1083 return new Date(this._startDate.getTime() + duration * 1000); 1084 } 1085 return null; 1086 } 1087 get duration() { 1088 if ("DURATION" in this.attr) { 1089 const duration = this.attr.decimalFloatingPoint("DURATION"); 1090 if (isFiniteNumber(duration)) { 1091 return duration; 1092 } 1093 } else if (this._endDate) { 1094 return (this._endDate.getTime() - this._startDate.getTime()) / 1000; 1095 } 1096 return null; 1097 } 1098 get plannedDuration() { 1099 if ("PLANNED-DURATION" in this.attr) { 1100 return this.attr.decimalFloatingPoint("PLANNED-DURATION"); 1101 } 1102 return null; 1103 } 1104 get endOnNext() { 1105 return this.attr.bool("END-ON-NEXT"); 1106 } 1107 get isValid() { 1108 return !!this.id && !this._badValueForSameId && isFiniteNumber(this.startDate.getTime()) && (this.duration === null || this.duration >= 0) && (!this.endOnNext || !!this.class); 1109 } 1110} 1111 1112class LoadStats { 1113 constructor() { 1114 this.aborted = false; 1115 this.loaded = 0; 1116 this.retry = 0; 1117 this.total = 0; 1118 this.chunkCount = 0; 1119 this.bwEstimate = 0; 1120 this.loading = { 1121 start: 0, 1122 first: 0, 1123 end: 0 1124 }; 1125 this.parsing = { 1126 start: 0, 1127 end: 0 1128 }; 1129 this.buffering = { 1130 start: 0, 1131 first: 0, 1132 end: 0 1133 }; 1134 } 1135} 1136 1137var ElementaryStreamTypes = { 1138 AUDIO: "audio", 1139 VIDEO: "video", 1140 AUDIOVIDEO: "audiovideo" 1141}; 1142class BaseSegment { 1143 constructor(baseurl) { 1144 this._byteRange = null; 1145 this._url = null; 1146 // baseurl is the URL to the playlist 1147 this.baseurl = void 0; 1148 // relurl is the portion of the URL that comes from inside the playlist. 1149 this.relurl = void 0; 1150 // Holds the types of data this fragment supports 1151 this.elementaryStreams = { 1152 [ElementaryStreamTypes.AUDIO]: null, 1153 [ElementaryStreamTypes.VIDEO]: null, 1154 [ElementaryStreamTypes.AUDIOVIDEO]: null 1155 }; 1156 this.baseurl = baseurl; 1157 } 1158 1159 // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array 1160 setByteRange(value, previous) { 1161 const params = value.split('@', 2); 1162 let start; 1163 if (params.length === 1) { 1164 start = (previous == null ? void 0 : previous.byteRangeEndOffset) || 0; 1165 } else { 1166 start = parseInt(params[1]); 1167 } 1168 this._byteRange = [start, parseInt(params[0]) + start]; 1169 } 1170 get byteRange() { 1171 if (!this._byteRange) { 1172 return []; 1173 } 1174 return this._byteRange; 1175 } 1176 get byteRangeStartOffset() { 1177 return this.byteRange[0]; 1178 } 1179 get byteRangeEndOffset() { 1180 return this.byteRange[1]; 1181 } 1182 get url() { 1183 if (!this._url && this.baseurl && this.relurl) { 1184 this._url = urlToolkitExports.buildAbsoluteURL(this.baseurl, this.relurl, { 1185 alwaysNormalize: true 1186 }); 1187 } 1188 return this._url || ''; 1189 }
1190 set url(value) { 1191 this._url = value; 1192 } 1193} 1194 1195/** 1196 * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}. 1197 */ 1198class Fragment extends BaseSegment { 1199 constructor(type, baseurl) { 1200 super(baseurl); 1201 this._decryptdata = null; 1202 this.rawProgramDateTime = null; 1203 this.programDateTime = null; 1204 this.tagList = []; 1205 // EXTINF has to be present for a m3u8 to be considered valid 1206 this.duration = 0; 1207 // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' 1208 this.sn = 0; 1209 // levelkeys are the EXT-X-KEY tags that apply to this segment for decryption 1210 // core difference from the private field _decryptdata is the lack of the initialized IV 1211 // _decryptdata will set the IV for this segment based on the segment number in the fragment 1212 this.levelkeys = void 0; 1213 // A string representing the fragment type 1214 this.type = void 0; 1215 // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading 1216 this.loader = null; 1217 // A reference to the key loader. Set while the key is loading, and removed afterwards. Used to abort key loading 1218 this.keyLoader = null; 1219 // The level/track index to which the fragment belongs 1220 this.level = -1; 1221 // The continuity counter of the fragment 1222 this.cc = 0; 1223 // The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. 1224 this.startPTS = void 0; 1225 // The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. 1226 this.endPTS = void 0; 1227 // The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete. 1228 this.startDTS = void 0; 1229 // The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete. 1230 this.endDTS = void 0; 1231 // The start time of the fragment, as listed in the manifest. Updated after transmux complete. 1232 this.start = 0; 1233 // Set by `updateFragPTSDTS` in level-helper 1234 this.deltaPTS = void 0; 1235 // The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. 1236 this.maxStartPTS = void 0; 1237 // The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. 1238 this.minEndPTS = void 0; 1239 // Load/parse timing information 1240 this.stats = new LoadStats(); 1241 // Init Segment bytes (unset for media segments) 1242 this.data = void 0; 1243 // A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered 1244 this.bitrateTest = false; 1245 // #EXTINF segment title 1246 this.title = null; 1247 // The Media Initialization Section for this segment 1248 this.initSegment = null; 1249 // Fragment is the last fragment in the media playlist 1250 this.endList = void 0; 1251 // Fragment is marked by an EXT-X-GAP tag indicating that it does not contain media data and should not be loaded 1252 this.gap = void 0; 1253 // Deprecated 1254 this.urlId = 0; 1255 this.type = type; 1256 } 1257 get decryptdata() { 1258 const { 1259 levelkeys 1260 } = this; 1261 if (!levelkeys && !this._decryptdata) { 1262 return null; 1263 } 1264 if (!this._decryptdata && this.levelkeys && !this.levelkeys.NONE) { 1265 const key = this.levelkeys.identity; 1266 if (key) { 1267 this._decryptdata = key.getDecryptData(this.sn); 1268 } else { 1269 const keyFormats = Object.keys(this.levelkeys); 1270 if (keyFormats.length === 1) { 1271 return this._decryptdata = this.levelkeys[keyFormats[0]].getDecryptData(this.sn); 1272 } 1273 } 1274 } 1275 return this._decryptdata; 1276 } 1277 get end() { 1278 return this.start + this.duration; 1279 } 1280 get endProgramDateTime() { 1281 if (this.programDateTime === null) { 1282 return null; 1283 } 1284 if (!isFiniteNumber(this.programDateTime)) { 1285 return null; 1286 } 1287 const duration = !isFiniteNumber(this.duration) ? 0 : this.duration; 1288 return this.programDateTime + duration * 1000; 1289 } 1290 get encrypted() { 1291 var _this$_decryptdata; 1292 // At the m3u8-parser level we need to add support for manifest signalled keyformats 1293 // when we want the fragment to start reporting that it is encrypted. 1294 // Currently, keyFormat will only be set for identity keys 1295 if ((_this$_decryptdata = this._decryptdata) != null && _this$_decryptdata.encrypted) { 1296 return true; 1297 } else if (this.levelkeys) {
1298 const keyFormats = Object.keys(this.levelkeys); 1299 const len = keyFormats.length; 1300 if (len > 1 || len === 1 && this.levelkeys[keyFormats[0]].encrypted) { 1301 return true; 1302 } 1303 } 1304 return false; 1305 } 1306 setKeyFormat(keyFormat) { 1307 if (this.levelkeys) { 1308 const key = this.levelkeys[keyFormat]; 1309 if (key && !this._decryptdata) { 1310 this._decryptdata = key.getDecryptData(this.sn); 1311 } 1312 } 1313 } 1314 abortRequests() { 1315 var _this$loader, _this$keyLoader; 1316 (_this$loader = this.loader) == null ? void 0 : _this$loader.abort(); 1317 (_this$keyLoader = this.keyLoader) == null ? void 0 : _this$keyLoader.abort(); 1318 } 1319 setElementaryStreamInfo(type, startPTS, endPTS, startDTS, endDTS, partial = false) { 1320 const { 1321 elementaryStreams 1322 } = this; 1323 const info = elementaryStreams[type]; 1324 if (!info) { 1325 elementaryStreams[type] = { 1326 startPTS, 1327 endPTS, 1328 startDTS, 1329 endDTS, 1330 partial 1331 }; 1332 return; 1333 } 1334 info.startPTS = Math.min(info.startPTS, startPTS); 1335 info.endPTS = Math.max(info.endPTS, endPTS); 1336 info.startDTS = Math.min(info.startDTS, startDTS); 1337 info.endDTS = Math.max(info.endDTS, endDTS); 1338 } 1339 clearElementaryStreamInfo() { 1340 const { 1341 elementaryStreams 1342 } = this; 1343 elementaryStreams[ElementaryStreamTypes.AUDIO] = null; 1344 elementaryStreams[ElementaryStreamTypes.VIDEO] = null; 1345 elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null; 1346 } 1347} 1348 1349/** 1350 * Object representing parsed data from an HLS Partial Segment. Found in {@link hls.js#LevelDetails.partList}. 1351 */ 1352class Part extends BaseSegment { 1353 constructor(partAttrs, frag, baseurl, index, previous) { 1354 super(baseurl); 1355 this.fragOffset = 0; 1356 this.duration = 0; 1357 this.gap = false; 1358 this.independent = false; 1359 this.relurl = void 0; 1360 this.fragment = void 0; 1361 this.index = void 0; 1362 this.stats = new LoadStats(); 1363 this.duration = partAttrs.decimalFloatingPoint('DURATION'); 1364 this.gap = partAttrs.bool('GAP'); 1365 this.independent = partAttrs.bool('INDEPENDENT'); 1366 this.relurl = partAttrs.enumeratedString('URI'); 1367 this.fragment = frag; 1368 this.index = index; 1369 const byteRange = partAttrs.enumeratedString('BYTERANGE'); 1370 if (byteRange) { 1371 this.setByteRange(byteRange, previous); 1372 } 1373 if (previous) { 1374 this.fragOffset = previous.fragOffset + previous.duration; 1375 } 1376 } 1377 get start() { 1378 return this.fragment.start + this.fragOffset; 1379 } 1380 get end() { 1381 return this.start + this.duration; 1382 } 1383 get loaded() { 1384 const { 1385 elementaryStreams 1386 } = this; 1387 return !!(elementaryStreams.audio || elementaryStreams.video || elementaryStreams.audiovideo); 1388 } 1389} 1390 1391const DEFAULT_TARGET_DURATION = 10; 1392 1393/** 1394 * Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}. 1395 */ 1396class LevelDetails { 1397 constructor(baseUrl) { 1398 this.PTSKnown = false; 1399 this.alignedSliding = false; 1400 this.averagetargetduration = void 0; 1401 this.endCC = 0; 1402 this.endSN = 0; 1403 this.fragments = void 0; 1404 this.fragmentHint = void 0; 1405 this.partList = null; 1406 this.dateRanges = void 0; 1407 this.live = true; 1408 this.ageHeader = 0; 1409 this.advancedDateTime = void 0; 1410 this.updated = true; 1411 this.advanced = true; 1412 this.availabilityDelay = void 0; 1413 // Manifest reload synchronization 1414 this.misses = 0; 1415 this.startCC = 0; 1416 this.startSN = 0; 1417 this.startTimeOffset = null; 1418 this.targetduration = 0; 1419 this.totalduration = 0; 1420 this.type = null; 1421 this.url = void 0; 1422 this.m3u8 = ''; 1423 this.version = null; 1424 this.canBlockReload = false; 1425 this.canSkipUntil = 0; 1426 this.canSkipDateRanges = false; 1427 this.skippedSegments = 0; 1428 this.recentlyRemovedDateranges = void 0; 1429 this.partHoldBack = 0; 1430 this.holdBack = 0; 1431 this.partTarget = 0; 1432 this.preloadHint = void 0; 1433 this.renditionReports = void 0; 1434 this.tuneInGoal = 0; 1435 this.deltaUpdateFailed = void 0; 1436 this.driftStartTime = 0; 1437 this.driftEndTime = 0; 1438 this.driftStart = 0; 1439 this.driftEnd = 0; 1440 this.encryptedFragments = void 0; 1441 this.playlistParsingError = null; 1442 this.variableList = null; 1443 this.hasVariableRefs = false;
vendor: 4,705 bytes, lines 1444-1594
1444 this.fragments = []; 1445 this.encryptedFragments = []; 1446 this.dateRanges = {}; 1447 this.url = baseUrl; 1448 } 1449 reloaded(previous) { 1450 if (!previous) { 1451 this.advanced = true; 1452 this.updated = true; 1453 return; 1454 } 1455 const partSnDiff = this.lastPartSn - previous.lastPartSn; 1456 const partIndexDiff = this.lastPartIndex - previous.lastPartIndex; 1457 this.updated = this.endSN !== previous.endSN || !!partIndexDiff || !!partSnDiff || !this.live; 1458 this.advanced = this.endSN > previous.endSN || partSnDiff > 0 || partSnDiff === 0 && partIndexDiff > 0; 1459 if (this.updated || this.advanced) { 1460 this.misses = Math.floor(previous.misses * 0.6); 1461 } else { 1462 this.misses = previous.misses + 1; 1463 } 1464 this.availabilityDelay = previous.availabilityDelay; 1465 } 1466 get hasProgramDateTime() { 1467 if (this.fragments.length) { 1468 return isFiniteNumber(this.fragments[this.fragments.length - 1].programDateTime); 1469 } 1470 return false; 1471 } 1472 get levelTargetDuration() { 1473 return this.averagetargetduration || this.targetduration || DEFAULT_TARGET_DURATION; 1474 } 1475 get drift() { 1476 const runTime = this.driftEndTime - this.driftStartTime; 1477 if (runTime > 0) { 1478 const runDuration = this.driftEnd - this.driftStart; 1479 return runDuration * 1000 / runTime; 1480 } 1481 return 1; 1482 } 1483 get edge() { 1484 return this.partEnd || this.fragmentEnd; 1485 } 1486 get partEnd() { 1487 var _this$partList; 1488 if ((_this$partList = this.partList) != null && _this$partList.length) { 1489 return this.partList[this.partList.length - 1].end; 1490 } 1491 return this.fragmentEnd; 1492 } 1493 get fragmentEnd() { 1494 var _this$fragments; 1495 if ((_this$fragments = this.fragments) != null && _this$fragments.length) { 1496 return this.fragments[this.fragments.length - 1].end; 1497 } 1498 return 0; 1499 } 1500 get age() { 1501 if (this.advancedDateTime) { 1502 return Math.max(Date.now() - this.advancedDateTime, 0) / 1000; 1503 } 1504 return 0; 1505 } 1506 get lastPartIndex() { 1507 var _this$partList2; 1508 if ((_this$partList2 = this.partList) != null && _this$partList2.length) { 1509 return this.partList[this.partList.length - 1].index; 1510 } 1511 return -1; 1512 } 1513 get lastPartSn() { 1514 var _this$partList3; 1515 if ((_this$partList3 = this.partList) != null && _this$partList3.length) { 1516 return this.partList[this.partList.length - 1].fragment.sn; 1517 } 1518 return this.endSN; 1519 } 1520} 1521 1522function base64Decode(base64encodedStr) { 1523 return Uint8Array.from(atob(base64encodedStr), c => c.charCodeAt(0)); 1524} 1525 1526function getKeyIdBytes(str) { 1527 const keyIdbytes = strToUtf8array(str).subarray(0, 16); 1528 const paddedkeyIdbytes = new Uint8Array(16); 1529 paddedkeyIdbytes.set(keyIdbytes, 16 - keyIdbytes.length); 1530 return paddedkeyIdbytes; 1531} 1532function changeEndianness(keyId) { 1533 const swap = function swap(array, from, to) { 1534 const cur = array[from]; 1535 array[from] = array[to]; 1536 array[to] = cur; 1537 }; 1538 swap(keyId, 0, 3); 1539 swap(keyId, 1, 2); 1540 swap(keyId, 4, 5); 1541 swap(keyId, 6, 7); 1542} 1543function convertDataUriToArrayBytes(uri) { 1544 // data:[<media type][;attribute=value][;base64],<data> 1545 const colonsplit = uri.split(':'); 1546 let keydata = null; 1547 if (colonsplit[0] === 'data' && colonsplit.length === 2) { 1548 const semicolonsplit = colonsplit[1].split(';'); 1549 const commasplit = semicolonsplit[semicolonsplit.length - 1].split(','); 1550 if (commasplit.length === 2) { 1551 const isbase64 = commasplit[0] === 'base64'; 1552 const data = commasplit[1]; 1553 if (isbase64) { 1554 semicolonsplit.splice(-1, 1); // remove from processing 1555 keydata = base64Decode(data); 1556 } else { 1557 keydata = getKeyIdBytes(data); 1558 } 1559 } 1560 } 1561 return keydata; 1562} 1563function strToUtf8array(str) { 1564 return Uint8Array.from(unescape(encodeURIComponent(str)), c => c.charCodeAt(0)); 1565} 1566 1567/** returns `undefined` is `self` is missing, e.g. in node */ 1568const optionalSelf = typeof self !== 'undefined' ? self : undefined; 1569 1570/** 1571 * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess 1572 */ 1573var KeySystems = { 1574 CLEARKEY: "org.w3.clearkey", 1575 FAIRPLAY: "com.apple.fps", 1576 PLAYREADY: "com.microsoft.playready", 1577 WIDEVINE: "com.widevine.alpha" 1578}; 1579 1580// Playlist #EXT-X-KEY KEYFORMAT values 1581var KeySystemFormats = { 1582 CLEARKEY: "org.w3.clearkey", 1583 FAIRPLAY: "com.apple.streamingkeydelivery", 1584 PLAYREADY: "com.microsoft.playready", 1585 WIDEVINE: "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" 1586}; 1587function keySystemFormatToKeySystemDomain(format) { 1588 switch (format) { 1589 case KeySystemFormats.FAIRPLAY: 1590 return KeySystems.FAIRPLAY; 1591 case KeySystemFormats.PLAYREADY: 1592 return KeySystems.PLAYREADY; 1593 case KeySystemFormats.WIDEVINE: 1594 return KeySystems.WIDEVINE;
vendor: 4,139 bytes, lines 1595-1707
1595 case KeySystemFormats.CLEARKEY: 1596 return KeySystems.CLEARKEY; 1597 } 1598} 1599 1600// System IDs for which we can extract a key ID from "encrypted" event PSSH 1601var KeySystemIds = { 1602 WIDEVINE: "edef8ba979d64acea3c827dcd51d21ed" 1603}; 1604function keySystemIdToKeySystemDomain(systemId) { 1605 if (systemId === KeySystemIds.WIDEVINE) { 1606 return KeySystems.WIDEVINE; 1607 // } else if (systemId === KeySystemIds.PLAYREADY) { 1608 // return KeySystems.PLAYREADY; 1609 // } else if (systemId === KeySystemIds.CENC || systemId === KeySystemIds.CLEARKEY) { 1610 // return KeySystems.CLEARKEY; 1611 } 1612} 1613function keySystemDomainToKeySystemFormat(keySystem) { 1614 switch (keySystem) { 1615 case KeySystems.FAIRPLAY: 1616 return KeySystemFormats.FAIRPLAY; 1617 case KeySystems.PLAYREADY: 1618 return KeySystemFormats.PLAYREADY; 1619 case KeySystems.WIDEVINE: 1620 return KeySystemFormats.WIDEVINE; 1621 case KeySystems.CLEARKEY: 1622 return KeySystemFormats.CLEARKEY; 1623 } 1624} 1625function getKeySystemsForConfig(config) { 1626 const { 1627 drmSystems, 1628 widevineLicenseUrl 1629 } = config; 1630 const keySystemsToAttempt = drmSystems ? [KeySystems.FAIRPLAY, KeySystems.WIDEVINE, KeySystems.PLAYREADY, KeySystems.CLEARKEY].filter(keySystem => !!drmSystems[keySystem]) : []; 1631 if (!keySystemsToAttempt[KeySystems.WIDEVINE] && widevineLicenseUrl) { 1632 keySystemsToAttempt.push(KeySystems.WIDEVINE); 1633 } 1634 return keySystemsToAttempt; 1635} 1636const requestMediaKeySystemAccess = function (_optionalSelf$navigat) { 1637 if (optionalSelf != null && (_optionalSelf$navigat = optionalSelf.navigator) != null && _optionalSelf$navigat.requestMediaKeySystemAccess) { 1638 return self.navigator.requestMediaKeySystemAccess.bind(self.navigator); 1639 } else { 1640 return null; 1641 } 1642}(); 1643 1644/** 1645 * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration 1646 */ 1647function getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, drmSystemOptions) { 1648 let initDataTypes; 1649 switch (keySystem) { 1650 case KeySystems.FAIRPLAY: 1651 initDataTypes = ['cenc', 'sinf']; 1652 break; 1653 case KeySystems.WIDEVINE: 1654 case KeySystems.PLAYREADY: 1655 initDataTypes = ['cenc']; 1656 break; 1657 case KeySystems.CLEARKEY: 1658 initDataTypes = ['cenc', 'keyids']; 1659 break; 1660 default: 1661 throw new Error(`Unknown key-system: ${keySystem}`); 1662 } 1663 return createMediaKeySystemConfigurations(initDataTypes, audioCodecs, videoCodecs, drmSystemOptions); 1664} 1665function createMediaKeySystemConfigurations(initDataTypes, audioCodecs, videoCodecs, drmSystemOptions) { 1666 const baseConfig = { 1667 initDataTypes: initDataTypes, 1668 persistentState: drmSystemOptions.persistentState || 'optional', 1669 distinctiveIdentifier: drmSystemOptions.distinctiveIdentifier || 'optional', 1670 sessionTypes: drmSystemOptions.sessionTypes || [drmSystemOptions.sessionType || 'temporary'], 1671 audioCapabilities: audioCodecs.map(codec => ({ 1672 contentType: `audio/mp4; codecs="${codec}"`, 1673 robustness: drmSystemOptions.audioRobustness || '', 1674 encryptionScheme: drmSystemOptions.audioEncryptionScheme || null 1675 })), 1676 videoCapabilities: videoCodecs.map(codec => ({ 1677 contentType: `video/mp4; codecs="${codec}"`, 1678 robustness: drmSystemOptions.videoRobustness || '', 1679 encryptionScheme: drmSystemOptions.videoEncryptionScheme || null 1680 })) 1681 }; 1682 return [baseConfig]; 1683} 1684 1685function sliceUint8(array, start, end) { 1686 // @ts-expect-error This polyfills IE11 usage of Uint8Array slice. 1687 // It always exists in the TypeScript definition so fails, but it fails at runtime on IE11. 1688 return Uint8Array.prototype.slice ? array.slice(start, end) : new Uint8Array(Array.prototype.slice.call(array, start, end)); 1689} 1690 1691// breaking up those two types in order to clarify what is happening in the decoding path. 1692 1693/** 1694 * Returns true if an ID3 header can be found at offset in data 1695 * @param data - The data to search 1696 * @param offset - The offset at which to start searching 1697 */ 1698const isHeader$2 = (data, offset) => { 1699 /* 1700 * http://id3.org/id3v2.3.0 1701 * [0] = 'I' 1702 * [1] = 'D' 1703 * [2] = '3' 1704 * [3,4] = {Version} 1705 * [5] = {Flags} 1706 * [6-9] = {ID3 Size} 1707 *
1708 * An ID3v2 tag can be detected with the following pattern: 1709 * $49 44 33 yy yy xx zz zz zz zz 1710 * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80 1711 */ 1712 if (offset + 10 <= data.length) { 1713 // look for 'ID3' identifier 1714 if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) { 1715 // check version is within range 1716 if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { 1717 // check size is within range 1718 if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { 1719 return true; 1720 } 1721 } 1722 } 1723 } 1724 return false; 1725}; 1726 1727/** 1728 * Returns true if an ID3 footer can be found at offset in data 1729 * @param data - The data to search 1730 * @param offset - The offset at which to start searching 1731 */ 1732const isFooter = (data, offset) => { 1733 /* 1734 * The footer is a copy of the header, but with a different identifier 1735 */ 1736 if (offset + 10 <= data.length) { 1737 // look for '3DI' identifier 1738 if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) { 1739 // check version is within range 1740 if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { 1741 // check size is within range 1742 if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { 1743 return true; 1744 } 1745 } 1746 } 1747 } 1748 return false; 1749}; 1750 1751/** 1752 * Returns any adjacent ID3 tags found in data starting at offset, as one block of data 1753 * @param data - The data to search in 1754 * @param offset - The offset at which to start searching 1755 * @returns the block of data containing any ID3 tags found 1756 * or *undefined* if no header is found at the starting offset 1757 */ 1758const getID3Data = (data, offset) => { 1759 const front = offset; 1760 let length = 0; 1761 while (isHeader$2(data, offset)) { 1762 // ID3 header is 10 bytes 1763 length += 10; 1764 const size = readSize(data, offset + 6); 1765 length += size; 1766 if (isFooter(data, offset + 10)) { 1767 // ID3 footer is 10 bytes 1768 length += 10; 1769 } 1770 offset += length; 1771 } 1772 if (length > 0) { 1773 return data.subarray(front, front + length); 1774 } 1775 return undefined; 1776}; 1777const readSize = (data, offset) => { 1778 let size = 0; 1779 size = (data[offset] & 0x7f) << 21; 1780 size |= (data[offset + 1] & 0x7f) << 14; 1781 size |= (data[offset + 2] & 0x7f) << 7; 1782 size |= data[offset + 3] & 0x7f; 1783 return size; 1784}; 1785const canParse$2 = (data, offset) => { 1786 return isHeader$2(data, offset) && readSize(data, offset + 6) + 10 <= data.length - offset; 1787}; 1788 1789/** 1790 * Searches for the Elementary Stream timestamp found in the ID3 data chunk 1791 * @param data - Block of data containing one or more ID3 tags 1792 */ 1793const getTimeStamp = data => { 1794 const frames = getID3Frames(data); 1795 for (let i = 0; i < frames.length; i++) { 1796 const frame = frames[i]; 1797 if (isTimeStampFrame(frame)) { 1798 return readTimeStamp(frame); 1799 } 1800 } 1801 return undefined; 1802}; 1803 1804/** 1805 * Returns true if the ID3 frame is an Elementary Stream timestamp frame 1806 */ 1807const isTimeStampFrame = frame => { 1808 return frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp'; 1809}; 1810const getFrameData = data => { 1811 /* 1812 Frame ID $xx xx xx xx (four characters) 1813 Size $xx xx xx xx 1814 Flags $xx xx 1815 */ 1816 const type = String.fromCharCode(data[0], data[1], data[2], data[3]); 1817 const size = readSize(data, 4); 1818 1819 // skip frame id, size, and flags 1820 const offset = 10; 1821 return { 1822 type, 1823 size, 1824 data: data.subarray(offset, offset + size) 1825 }; 1826}; 1827 1828/** 1829 * Returns an array of ID3 frames found in all the ID3 tags in the id3Data 1830 * @param id3Data - The ID3 data containing one or more ID3 tags 1831 */ 1832const getID3Frames = id3Data => { 1833 let offset = 0; 1834 const frames = []; 1835 while (isHeader$2(id3Data, offset)) { 1836 const size = readSize(id3Data, offset + 6); 1837 // skip past ID3 header 1838 offset += 10; 1839 const end = offset + size; 1840 // loop through frames in the ID3 tag 1841 while (offset + 8 < end) { 1842 const frameData = getFrameData(id3Data.subarray(offset)); 1843 const frame = decodeFrame(frameData); 1844 if (frame) { 1845 frames.push(frame); 1846 } 1847 1848 // skip frame header and frame data 1849 offset += frameData.size + 10; 1850 } 1851 if (isFooter(id3Data, offset)) { 1852 offset += 10; 1853 } 1854 } 1855 return frames; 1856}; 1857const decodeFrame = frame => { 1858 if (frame.type === 'PRIV') { 1859 return decodePrivFrame(frame); 1860 } else if (frame.type[0] === 'W') { 1861 return decodeURLFrame(frame); 1862 } 1863 return decodeTextFrame(frame); 1864}; 1865const decodePrivFrame = frame => { 1866 /* 1867 Format: <text string>\0<binary data> 1868 */
1869 if (frame.size < 2) { 1870 return undefined; 1871 } 1872 const owner = utf8ArrayToStr(frame.data, true); 1873 const privateData = new Uint8Array(frame.data.subarray(owner.length + 1)); 1874 return { 1875 key: frame.type, 1876 info: owner, 1877 data: privateData.buffer 1878 }; 1879}; 1880const decodeTextFrame = frame => { 1881 if (frame.size < 2) { 1882 return undefined; 1883 } 1884 if (frame.type === 'TXXX') { 1885 /* 1886 Format: 1887 [0] = {Text Encoding} 1888 [1-?] = {Description}\0{Value} 1889 */ 1890 let index = 1; 1891 const description = utf8ArrayToStr(frame.data.subarray(index), true); 1892 index += description.length + 1; 1893 const value = utf8ArrayToStr(frame.data.subarray(index)); 1894 return { 1895 key: frame.type, 1896 info: description, 1897 data: value 1898 }; 1899 } 1900 /* 1901 Format: 1902 [0] = {Text Encoding} 1903 [1-?] = {Value} 1904 */ 1905 const text = utf8ArrayToStr(frame.data.subarray(1)); 1906 return { 1907 key: frame.type, 1908 data: text 1909 }; 1910}; 1911const decodeURLFrame = frame => { 1912 if (frame.type === 'WXXX') { 1913 /* 1914 Format: 1915 [0] = {Text Encoding} 1916 [1-?] = {Description}\0{URL} 1917 */ 1918 if (frame.size < 2) { 1919 return undefined; 1920 } 1921 let index = 1; 1922 const description = utf8ArrayToStr(frame.data.subarray(index), true); 1923 index += description.length + 1; 1924 const value = utf8ArrayToStr(frame.data.subarray(index)); 1925 return { 1926 key: frame.type, 1927 info: description, 1928 data: value 1929 }; 1930 } 1931 /* 1932 Format: 1933 [0-?] = {URL} 1934 */ 1935 const url = utf8ArrayToStr(frame.data); 1936 return { 1937 key: frame.type, 1938 data: url 1939 }; 1940}; 1941const readTimeStamp = timeStampFrame => { 1942 if (timeStampFrame.data.byteLength === 8) { 1943 const data = new Uint8Array(timeStampFrame.data); 1944 // timestamp is 33 bit expressed as a big-endian eight-octet number, 1945 // with the upper 31 bits set to zero. 1946 const pts33Bit = data[3] & 0x1; 1947 let timestamp = (data[4] << 23) + (data[5] << 15) + (data[6] << 7) + data[7]; 1948 timestamp /= 45; 1949 if (pts33Bit) { 1950 timestamp += 47721858.84; 1951 } // 2^32 / 90 1952 1953 return Math.round(timestamp); 1954 } 1955 return undefined; 1956}; 1957 1958// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197 1959// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt 1960/* utf.js - UTF-8 <=> UTF-16 convertion 1961 * 1962 * Copyright (C) 1999 Masanao Izumo <[email protected]> 1963 * Version: 1.0 1964 * LastModified: Dec 25 1999 1965 * This library is free. You can redistribute it and/or modify it. 1966 */ 1967const utf8ArrayToStr = (array, exitOnNull = false) => { 1968 const decoder = getTextDecoder(); 1969 if (decoder) { 1970 const decoded = decoder.decode(array); 1971 if (exitOnNull) { 1972 // grab up to the first null 1973 const idx = decoded.indexOf('\0'); 1974 return idx !== -1 ? decoded.substring(0, idx) : decoded; 1975 } 1976 1977 // remove any null characters 1978 return decoded.replace(/\0/g, ''); 1979 } 1980 const len = array.length; 1981 let c; 1982 let char2; 1983 let char3; 1984 let out = ''; 1985 let i = 0; 1986 while (i < len) { 1987 c = array[i++]; 1988 if (c === 0x00 && exitOnNull) { 1989 return out; 1990 } else if (c === 0x00 || c === 0x03) { 1991 // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it 1992 continue; 1993 } 1994 switch (c >> 4) { 1995 case 0: 1996 case 1: 1997 case 2: 1998 case 3: 1999 case 4: 2000 case 5: 2001 case 6: 2002 case 7: 2003 // 0xxxxxxx 2004 out += String.fromCharCode(c); 2005 break; 2006 case 12: 2007 case 13: 2008 // 110x xxxx 10xx xxxx 2009 char2 = array[i++]; 2010 out += String.fromCharCode((c & 0x1f) << 6 | char2 & 0x3f); 2011 break; 2012 case 14: 2013 // 1110 xxxx 10xx xxxx 10xx xxxx 2014 char2 = array[i++]; 2015 char3 = array[i++]; 2016 out += String.fromCharCode((c & 0x0f) << 12 | (char2 & 0x3f) << 6 | (char3 & 0x3f) << 0); 2017 break; 2018 } 2019 } 2020 return out; 2021}; 2022let decoder; 2023function getTextDecoder() { 2024 // On Play Station 4, TextDecoder is defined but partially implemented. 2025 // Manual decoding option is preferable 2026 if (navigator.userAgent.includes('PlayStation 4')) { 2027 return; 2028 } 2029 if (!decoder && typeof self.TextDecoder !== 'undefined') { 2030 decoder = new self.TextDecoder('utf-8'); 2031 } 2032 return decoder; 2033} 2034 2035/** 2036 * hex dump helper class 2037 */ 2038 2039const Hex = { 2040 hexDump: function (array) { 2041 let str = ''; 2042 for (let i = 0; i < array.length; i++) { 2043 let h = array[i].toString(16); 2044 if (h.length < 2) { 2045 h = '0' + h; 2046 } 2047 str += h; 2048 } 2049 return str; 2050 } 2051}; 2052 2053const UINT32_MAX$1 = Math.pow(2, 32) - 1; 2054const push = [].push; 2055 2056// We are using fixed track IDs for driving the MP4 remuxer 2057// instead of following the TS PIDs. 2058// There is no reason not to do this and some browsers/SourceBuffer-demuxers 2059// may not like if there are TrackID "switches" 2060// See https://github.com/video-dev/hls.js/issues/1331 2061// Here we are mapping our internal track types to constant MP4 track IDs 2062// With MSE currently one can only have one track of each, and we are muxing 2063// whatever video/audio rendition in them. 2064const RemuxerTrackIdConfig = { 2065 video: 1, 2066 audio: 2, 2067 id3: 3, 2068 text: 4 2069}; 2070function bin2str(data) { 2071 return String.fromCharCode.apply(null, data); 2072} 2073function readUint16(buffer, offset) { 2074 const val = buffer[offset] << 8 | buffer[offset + 1]; 2075 return val < 0 ? 65536 + val : val; 2076} 2077function readUint32(buffer, offset) { 2078 const val = readSint32(buffer, offset); 2079 return val < 0 ? 4294967296 + val : val; 2080} 2081function readUint64(buffer, offset) { 2082 let result = readUint32(buffer, offset); 2083 result *= Math.pow(2, 32); 2084 result += readUint32(buffer, offset + 4); 2085 return result; 2086} 2087function readSint32(buffer, offset) { 2088 return buffer[offset] << 24 | buffer[offset + 1] << 16 | buffer[offset + 2] << 8 | buffer[offset + 3]; 2089} 2090function writeUint32(buffer, offset, value) { 2091 buffer[offset] = value >> 24; 2092 buffer[offset + 1] = value >> 16 & 0xff;
vendor: 4,988 bytes, lines 2093-2253
2093 buffer[offset + 2] = value >> 8 & 0xff; 2094 buffer[offset + 3] = value & 0xff; 2095} 2096 2097// Find "moof" box 2098function hasMoofData(data) { 2099 const end = data.byteLength; 2100 for (let i = 0; i < end;) { 2101 const size = readUint32(data, i); 2102 if (size > 8 && data[i + 4] === 0x6d && data[i + 5] === 0x6f && data[i + 6] === 0x6f && data[i + 7] === 0x66) { 2103 return true; 2104 } 2105 i = size > 1 ? i + size : end; 2106 } 2107 return false; 2108} 2109 2110// Find the data for a box specified by its path 2111function findBox(data, path) { 2112 const results = []; 2113 if (!path.length) { 2114 // short-circuit the search for empty paths 2115 return results; 2116 } 2117 const end = data.byteLength; 2118 for (let i = 0; i < end;) { 2119 const size = readUint32(data, i); 2120 const type = bin2str(data.subarray(i + 4, i + 8)); 2121 const endbox = size > 1 ? i + size : end; 2122 if (type === path[0]) { 2123 if (path.length === 1) { 2124 // this is the end of the path and we've found the box we were 2125 // looking for 2126 results.push(data.subarray(i + 8, endbox)); 2127 } else { 2128 // recursively search for the next box along the path 2129 const subresults = findBox(data.subarray(i + 8, endbox), path.slice(1)); 2130 if (subresults.length) { 2131 push.apply(results, subresults); 2132 } 2133 } 2134 } 2135 i = endbox; 2136 } 2137 2138 // we've finished searching all of data 2139 return results; 2140} 2141function parseSegmentIndex(sidx) { 2142 const references = []; 2143 const version = sidx[0]; 2144 2145 // set initial offset, we skip the reference ID (not needed) 2146 let index = 8; 2147 const timescale = readUint32(sidx, index); 2148 index += 4; 2149 let earliestPresentationTime = 0; 2150 let firstOffset = 0; 2151 if (version === 0) { 2152 earliestPresentationTime = readUint32(sidx, index); 2153 firstOffset = readUint32(sidx, index + 4); 2154 index += 8; 2155 } else { 2156 earliestPresentationTime = readUint64(sidx, index); 2157 firstOffset = readUint64(sidx, index + 8); 2158 index += 16; 2159 } 2160 2161 // skip reserved 2162 index += 2; 2163 let startByte = sidx.length + firstOffset; 2164 const referencesCount = readUint16(sidx, index); 2165 index += 2; 2166 for (let i = 0; i < referencesCount; i++) { 2167 let referenceIndex = index; 2168 const referenceInfo = readUint32(sidx, referenceIndex); 2169 referenceIndex += 4; 2170 const referenceSize = referenceInfo & 0x7fffffff; 2171 const referenceType = (referenceInfo & 0x80000000) >>> 31; 2172 if (referenceType === 1) { 2173 logger.warn('SIDX has hierarchical references (not supported)'); 2174 return null; 2175 } 2176 const subsegmentDuration = readUint32(sidx, referenceIndex); 2177 referenceIndex += 4; 2178 references.push({ 2179 referenceSize, 2180 subsegmentDuration, 2181 // unscaled 2182 info: { 2183 duration: subsegmentDuration / timescale, 2184 start: startByte, 2185 end: startByte + referenceSize - 1 2186 } 2187 }); 2188 startByte += referenceSize; 2189 2190 // Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits 2191 // for |sapDelta|. 2192 referenceIndex += 4; 2193 2194 // skip to next ref 2195 index = referenceIndex; 2196 } 2197 return { 2198 earliestPresentationTime, 2199 timescale, 2200 version, 2201 referencesCount, 2202 references 2203 }; 2204} 2205 2206/** 2207 * Parses an MP4 initialization segment and extracts stream type and 2208 * timescale values for any declared tracks. Timescale values indicate the 2209 * number of clock ticks per second to assume for time-based values 2210 * elsewhere in the MP4. 2211 * 2212 * To determine the start time of an MP4, you need two pieces of 2213 * information: the timescale unit and the earliest base media decode 2214 * time. Multiple timescales can be specified within an MP4 but the 2215 * base media decode time is always expressed in the timescale from 2216 * the media header box for the track: 2217 * ``` 2218 * moov > trak > mdia > mdhd.timescale 2219 * moov > trak > mdia > hdlr 2220 * ``` 2221 * @param initSegment the bytes of the init segment 2222 * @returns a hash of track type to timescale values or null if 2223 * the init segment is malformed. 2224 */ 2225 2226function parseInitSegment(initSegment) { 2227 const result = []; 2228 const traks = findBox(initSegment, ['moov', 'trak']); 2229 for (let i = 0; i < traks.length; i++) { 2230 const trak = traks[i]; 2231 const tkhd = findBox(trak, ['tkhd'])[0]; 2232 if (tkhd) { 2233 let version = tkhd[0]; 2234 const trackId = readUint32(tkhd, version === 0 ? 12 : 20); 2235 const mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 2236 if (mdhd) { 2237 version = mdhd[0]; 2238 const timescale = readUint32(mdhd, version === 0 ? 12 : 20); 2239 const hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; 2240 if (hdlr) { 2241 const hdlrType = bin2str(hdlr.subarray(8, 12)); 2242 const type = { 2243 soun: ElementaryStreamTypes.AUDIO, 2244 vide: ElementaryStreamTypes.VIDEO 2245 }[hdlrType]; 2246 if (type) { 2247 // Parse codec details 2248 const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 2249 const stsdData = parseStsd(stsd); 2250 result[trackId] = { 2251 timescale, 2252 type 2253 };
2254 result[type] = _objectSpread2({ 2255 timescale, 2256 id: trackId 2257 }, stsdData); 2258 } 2259 } 2260 } 2261 } 2262 } 2263 const trex = findBox(initSegment, ['moov', 'mvex', 'trex']); 2264 trex.forEach(trex => { 2265 const trackId = readUint32(trex, 4); 2266 const track = result[trackId]; 2267 if (track) { 2268 track.default = { 2269 duration: readUint32(trex, 12), 2270 flags: readUint32(trex, 20) 2271 }; 2272 } 2273 }); 2274 return result; 2275} 2276function parseStsd(stsd) { 2277 const sampleEntries = stsd.subarray(8); 2278 const sampleEntriesEnd = sampleEntries.subarray(8 + 78); 2279 const fourCC = bin2str(sampleEntries.subarray(4, 8)); 2280 let codec = fourCC; 2281 const encrypted = fourCC === 'enca' || fourCC === 'encv'; 2282 if (encrypted) { 2283 const encBox = findBox(sampleEntries, [fourCC])[0]; 2284 const encBoxChildren = encBox.subarray(fourCC === 'enca' ? 28 : 78); 2285 const sinfs = findBox(encBoxChildren, ['sinf']); 2286 sinfs.forEach(sinf => { 2287 const schm = findBox(sinf, ['schm'])[0]; 2288 if (schm) { 2289 const scheme = bin2str(schm.subarray(4, 8)); 2290 if (scheme === 'cbcs' || scheme === 'cenc') { 2291 const frma = findBox(sinf, ['frma'])[0]; 2292 if (frma) { 2293 // for encrypted content codec fourCC will be in frma 2294 codec = bin2str(frma); 2295 } 2296 } 2297 } 2298 }); 2299 } 2300 switch (codec) { 2301 case 'avc1': 2302 case 'avc2': 2303 case 'avc3': 2304 case 'avc4': 2305 { 2306 // extract profile + compatibility + level out of avcC box 2307 const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0]; 2308 codec += '.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]); 2309 break; 2310 } 2311 case 'mp4a': 2312 { 2313 const codecBox = findBox(sampleEntries, [fourCC])[0]; 2314 const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0]; 2315 if (esdsBox && esdsBox.length > 12) { 2316 let i = 4; 2317 // ES Descriptor tag 2318 if (esdsBox[i++] !== 0x03) { 2319 break; 2320 } 2321 i = skipBERInteger(esdsBox, i); 2322 i += 2; // skip es_id; 2323 const flags = esdsBox[i++]; 2324 if (flags & 0x80) { 2325 i += 2; // skip dependency es_id 2326 } 2327 if (flags & 0x40) { 2328 i += esdsBox[i++]; // skip URL 2329 } 2330 // Decoder config descriptor 2331 if (esdsBox[i++] !== 0x04) { 2332 break; 2333 } 2334 i = skipBERInteger(esdsBox, i); 2335 const objectType = esdsBox[i++]; 2336 if (objectType === 0x40) { 2337 codec += '.' + toHex(objectType); 2338 } else { 2339 break; 2340 } 2341 i += 12; 2342 // Decoder specific info 2343 if (esdsBox[i++] !== 0x05) { 2344 break; 2345 } 2346 i = skipBERInteger(esdsBox, i); 2347 const firstByte = esdsBox[i++]; 2348 let audioObjectType = (firstByte & 0xf8) >> 3; 2349 if (audioObjectType === 31) { 2350 audioObjectType += 1 + ((firstByte & 0x7) << 3) + ((esdsBox[i] & 0xe0) >> 5); 2351 } 2352 codec += '.' + audioObjectType; 2353 } 2354 break; 2355 } 2356 case 'hvc1': 2357 case 'hev1': 2358 { 2359 const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0]; 2360 const profileByte = hvcCBox[1]; 2361 const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6]; 2362 const generalProfileIdc = profileByte & 0x1f; 2363 const profileCompat = readUint32(hvcCBox, 2); 2364 const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L'; 2365 const levelIDC = hvcCBox[12]; 2366 const constraintIndicator = hvcCBox.subarray(6, 12); 2367 codec += '.' + profileSpace + generalProfileIdc; 2368 codec += '.' + profileCompat.toString(16).toUpperCase(); 2369 codec += '.' + tierFlag + levelIDC; 2370 let constraintString = ''; 2371 for (let i = constraintIndicator.length; i--;) { 2372 const byte = constraintIndicator[i]; 2373 if (byte || constraintString) { 2374 const encodedByte = byte.toString(16).toUpperCase(); 2375 constraintString = '.' + encodedByte + constraintString; 2376 } 2377 } 2378 codec += constraintString; 2379 break; 2380 } 2381 case 'dvh1': 2382 case 'dvhe': 2383 { 2384 const dvcCBox = findBox(sampleEntriesEnd, ['dvcC'])[0]; 2385 const profile = dvcCBox[2] >> 1 & 0x7f; 2386 const level = dvcCBox[2] << 5 & 0x20 | dvcCBox[3] >> 3 & 0x1f; 2387 codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level); 2388 break; 2389 } 2390 case 'vp09': 2391 { 2392 const vpcCBox = findBox(sampleEntriesEnd, ['vpcC'])[0]; 2393 const profile = vpcCBox[4]; 2394 const level = vpcCBox[5]; 2395 const bitDepth = vpcCBox[6] >> 4 & 0x0f; 2396 codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level) + '.' + addLeadingZero(bitDepth); 2397 break; 2398 } 2399 case 'av01': 2400 { 2401 const av1CBox = findBox(sampleEntriesEnd, ['av1C'])[0]; 2402 const profile = av1CBox[1] >>> 5; 2403 const level = av1CBox[1] & 0x1f; 2404 const tierFlag = av1CBox[2] >>> 7 ? 'H' : 'M'; 2405 const highBitDepth = (av1CBox[2] & 0x40) >> 6; 2406 const twelveBit = (av1CBox[2] & 0x20) >> 5; 2407 const bitDepth = profile === 2 && highBitDepth ? twelveBit ? 12 : 10 : highBitDepth ? 10 : 8; 2408 const monochrome = (av1CBox[2] & 0x10) >> 4; 2409 const chromaSubsamplingX = (av1CBox[2] & 0x08) >> 3; 2410 const chromaSubsamplingY = (av1CBox[2] & 0x04) >> 2; 2411 const chromaSamplePosition = av1CBox[2] & 0x03; 2412 // TODO: parse color_description_present_flag 2413 // default it to BT.709/limited range for now 2414 // more info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax 2415 const colorPrimaries = 1; 2416 const transferCharacteristics = 1; 2417 const matrixCoefficients = 1; 2418 const videoFullRangeFlag = 0; 2419 codec += '.' + profile + '.' + addLeadingZero(level) + tierFlag + '.' + addLeadingZero(bitDepth) + '.' + monochrome + '.' + chromaSubsamplingX + chromaSubsamplingY + chromaSamplePosition + '.' + addLeadingZero(colorPrimaries) + '.' + addLeadingZero(transferCharacteristics) + '.' + addLeadingZero(matrixCoefficients) + '.' + videoFullRangeFlag; 2420 break; 2421 } 2422 } 2423 return { 2424 codec, 2425 encrypted 2426 }; 2427} 2428function skipBERInteger(bytes, i) { 2429 const limit = i + 5; 2430 while (bytes[i++] & 0x80 && i < limit) {} 2431 return i; 2432} 2433function toHex(x) { 2434 return ('0' + x.toString(16).toUpperCase()).slice(-2); 2435} 2436function addLeadingZero(num) { 2437 return (num < 10 ? '0' : '') + num; 2438} 2439function patchEncyptionData(initSegment, decryptdata) { 2440 if (!initSegment || !decryptdata) { 2441 return initSegment; 2442 } 2443 const keyId = decryptdata.keyId; 2444 if (keyId && decryptdata.isCommonEncryption) { 2445 const traks = findBox(initSegment, ['moov', 'trak']); 2446 traks.forEach(trak => { 2447 const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 2448 2449 // skip the sample entry count 2450 const sampleEntries = stsd.subarray(8); 2451 let encBoxes = findBox(sampleEntries, ['enca']); 2452 const isAudio = encBoxes.length > 0; 2453 if (!isAudio) { 2454 encBoxes = findBox(sampleEntries, ['encv']); 2455 }
2456 encBoxes.forEach(enc => { 2457 const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78); 2458 const sinfBoxes = findBox(encBoxChildren, ['sinf']); 2459 sinfBoxes.forEach(sinf => { 2460 const tenc = parseSinf(sinf); 2461 if (tenc) { 2462 // Look for default key id (keyID offset is always 8 within the tenc box): 2463 const tencKeyId = tenc.subarray(8, 24); 2464 if (!tencKeyId.some(b => b !== 0)) { 2465 logger.log(`[eme] Patching keyId in 'enc${isAudio ? 'a' : 'v'}>sinf>>tenc' box: ${Hex.hexDump(tencKeyId)} -> ${Hex.hexDump(keyId)}`); 2466 tenc.set(keyId, 8); 2467 } 2468 } 2469 }); 2470 }); 2471 }); 2472 } 2473 return initSegment; 2474} 2475function parseSinf(sinf) { 2476 const schm = findBox(sinf, ['schm'])[0]; 2477 if (schm) { 2478 const scheme = bin2str(schm.subarray(4, 8)); 2479 if (scheme === 'cbcs' || scheme === 'cenc') { 2480 return findBox(sinf, ['schi', 'tenc'])[0]; 2481 } 2482 } 2483 logger.error(`[eme] missing 'schm' box`); 2484 return null; 2485} 2486 2487/** 2488 * Determine the base media decode start time, in seconds, for an MP4 2489 * fragment. If multiple fragments are specified, the earliest time is 2490 * returned. 2491 * 2492 * The base media decode time can be parsed from track fragment 2493 * metadata: 2494 * ``` 2495 * moof > traf > tfdt.baseMediaDecodeTime 2496 * ``` 2497 * It requires the timescale value from the mdhd to interpret. 2498 * 2499 * @param initData - a hash of track type to timescale values 2500 * @param fmp4 - the bytes of the mp4 fragment 2501 * @returns the earliest base media decode start time for the 2502 * fragment, in seconds 2503 */ 2504function getStartDTS(initData, fmp4) { 2505 // we need info from two children of each track fragment box 2506 return findBox(fmp4, ['moof', 'traf']).reduce((result, traf) => { 2507 const tfdt = findBox(traf, ['tfdt'])[0]; 2508 const version = tfdt[0]; 2509 const start = findBox(traf, ['tfhd']).reduce((result, tfhd) => { 2510 // get the track id from the tfhd 2511 const id = readUint32(tfhd, 4); 2512 const track = initData[id]; 2513 if (track) { 2514 let baseTime = readUint32(tfdt, 4); 2515 if (version === 1) { 2516 // If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild. 2517 // This prevents large values from being used for initPTS, which can cause playlist sync issues. 2518 // https://github.com/video-dev/hls.js/issues/5303 2519 if (baseTime === UINT32_MAX$1) { 2520 logger.warn(`[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time`); 2521 return result; 2522 } 2523 baseTime *= UINT32_MAX$1 + 1; 2524 baseTime += readUint32(tfdt, 8); 2525 } 2526 // assume a 90kHz clock if no timescale was specified 2527 const scale = track.timescale || 90e3; 2528 // convert base time to seconds 2529 const startTime = baseTime / scale; 2530 if (isFiniteNumber(startTime) && (result === null || startTime < result)) { 2531 return startTime; 2532 } 2533 } 2534 return result; 2535 }, null); 2536 if (start !== null && isFiniteNumber(start) && (result === null || start < result)) { 2537 return start; 2538 } 2539 return result; 2540 }, null); 2541} 2542 2543/* 2544 For Reference: 2545 aligned(8) class TrackFragmentHeaderBox 2546 extends FullBox(‘tfhd’, 0, tf_flags){ 2547 unsigned int(32) track_ID; 2548 // all the following are optional fields 2549 unsigned int(64) base_data_offset; 2550 unsigned int(32) sample_description_index; 2551 unsigned int(32) default_sample_duration; 2552 unsigned int(32) default_sample_size; 2553 unsigned int(32) default_sample_flags 2554 } 2555 */ 2556function getDuration(data, initData) { 2557 let rawDuration = 0; 2558 let videoDuration = 0; 2559 let audioDuration = 0; 2560 const trafs = findBox(data, ['moof', 'traf']); 2561 for (let i = 0; i < trafs.length; i++) { 2562 const traf = trafs[i]; 2563 // There is only one tfhd & trun per traf 2564 // This is true for CMAF style content, and we should perhaps check the ftyp 2565 // and only look for a single trun then, but for ISOBMFF we should check 2566 // for multiple track runs. 2567 const tfhd = findBox(traf, ['tfhd'])[0]; 2568 // get the track id from the tfhd 2569 const id = readUint32(tfhd, 4); 2570 const track = initData[id]; 2571 if (!track) { 2572 continue; 2573 } 2574 const trackDefault = track.default; 2575 const tfhdFlags = readUint32(tfhd, 0) | (trackDefault == null ? void 0 : trackDefault.flags); 2576 let sampleDuration = trackDefault == null ? void 0 : trackDefault.duration; 2577 if (tfhdFlags & 0x000008) { 2578 // 0x000008 indicates the presence of the default_sample_duration field 2579 if (tfhdFlags & 0x000002) { 2580 // 0x000002 indicates the presence of the sample_description_index field, which precedes default_sample_duration 2581 // If present, the default_sample_duration exists at byte offset 12 2582 sampleDuration = readUint32(tfhd, 12); 2583 } else { 2584 // Otherwise, the duration is at byte offset 8 2585 sampleDuration = readUint32(tfhd, 8); 2586 } 2587 } 2588 // assume a 90kHz clock if no timescale was specified 2589 const timescale = track.timescale || 90e3; 2590 const truns = findBox(traf, ['trun']); 2591 for (let j = 0; j < truns.length; j++) { 2592 rawDuration = computeRawDurationFromSamples(truns[j]); 2593 if (!rawDuration && sampleDuration) { 2594 const sampleCount = readUint32(truns[j], 4);
2595 rawDuration = sampleDuration * sampleCount; 2596 } 2597 if (track.type === ElementaryStreamTypes.VIDEO) { 2598 videoDuration += rawDuration / timescale; 2599 } else if (track.type === ElementaryStreamTypes.AUDIO) { 2600 audioDuration += rawDuration / timescale; 2601 } 2602 } 2603 } 2604 if (videoDuration === 0 && audioDuration === 0) { 2605 // If duration samples are not available in the traf use sidx subsegment_duration 2606 let sidxMinStart = Infinity; 2607 let sidxMaxEnd = 0; 2608 let sidxDuration = 0; 2609 const sidxs = findBox(data, ['sidx']); 2610 for (let i = 0; i < sidxs.length; i++) { 2611 const sidx = parseSegmentIndex(sidxs[i]); 2612 if (sidx != null && sidx.references) { 2613 sidxMinStart = Math.min(sidxMinStart, sidx.earliestPresentationTime / sidx.timescale); 2614 const subSegmentDuration = sidx.references.reduce((dur, ref) => dur + ref.info.duration || 0, 0); 2615 sidxMaxEnd = Math.max(sidxMaxEnd, subSegmentDuration + sidx.earliestPresentationTime / sidx.timescale); 2616 sidxDuration = sidxMaxEnd - sidxMinStart; 2617 } 2618 } 2619 if (sidxDuration && isFiniteNumber(sidxDuration)) { 2620 return sidxDuration; 2621 } 2622 } 2623 if (videoDuration) { 2624 return videoDuration; 2625 } 2626 return audioDuration; 2627} 2628 2629/* 2630 For Reference: 2631 aligned(8) class TrackRunBox 2632 extends FullBox(‘trun’, version, tr_flags) { 2633 unsigned int(32) sample_count; 2634 // the following are optional fields 2635 signed int(32) data_offset; 2636 unsigned int(32) first_sample_flags; 2637 // all fields in the following array are optional 2638 { 2639 unsigned int(32) sample_duration; 2640 unsigned int(32) sample_size; 2641 unsigned int(32) sample_flags 2642 if (version == 0) 2643 { unsigned int(32) 2644 else 2645 { signed int(32) 2646 }[ sample_count ] 2647 } 2648 */ 2649function computeRawDurationFromSamples(trun) { 2650 const flags = readUint32(trun, 0); 2651 // Flags are at offset 0, non-optional sample_count is at offset 4. Therefore we start 8 bytes in. 2652 // Each field is an int32, which is 4 bytes 2653 let offset = 8; 2654 // data-offset-present flag 2655 if (flags & 0x000001) { 2656 offset += 4; 2657 } 2658 // first-sample-flags-present flag 2659 if (flags & 0x000004) { 2660 offset += 4; 2661 } 2662 let duration = 0; 2663 const sampleCount = readUint32(trun, 4); 2664 for (let i = 0; i < sampleCount; i++) { 2665 // sample-duration-present flag 2666 if (flags & 0x000100) { 2667 const sampleDuration = readUint32(trun, offset); 2668 duration += sampleDuration; 2669 offset += 4; 2670 } 2671 // sample-size-present flag 2672 if (flags & 0x000200) { 2673 offset += 4; 2674 } 2675 // sample-flags-present flag 2676 if (flags & 0x000400) { 2677 offset += 4; 2678 } 2679 // sample-composition-time-offsets-present flag 2680 if (flags & 0x000800) { 2681 offset += 4; 2682 } 2683 } 2684 return duration; 2685} 2686function offsetStartDTS(initData, fmp4, timeOffset) { 2687 findBox(fmp4, ['moof', 'traf']).forEach(traf => {
2688 findBox(traf, ['tfhd']).forEach(tfhd => { 2689 // get the track id from the tfhd 2690 const id = readUint32(tfhd, 4); 2691 const track = initData[id]; 2692 if (!track) { 2693 return; 2694 } 2695 // assume a 90kHz clock if no timescale was specified 2696 const timescale = track.timescale || 90e3; 2697 // get the base media decode time from the tfdt 2698 findBox(traf, ['tfdt']).forEach(tfdt => { 2699 const version = tfdt[0]; 2700 const offset = timeOffset * timescale; 2701 if (offset) { 2702 let baseMediaDecodeTime = readUint32(tfdt, 4); 2703 if (version === 0) { 2704 baseMediaDecodeTime -= offset; 2705 baseMediaDecodeTime = Math.max(baseMediaDecodeTime, 0); 2706 writeUint32(tfdt, 4, baseMediaDecodeTime); 2707 } else { 2708 baseMediaDecodeTime *= Math.pow(2, 32); 2709 baseMediaDecodeTime += readUint32(tfdt, 8); 2710 baseMediaDecodeTime -= offset; 2711 baseMediaDecodeTime = Math.max(baseMediaDecodeTime, 0); 2712 const upper = Math.floor(baseMediaDecodeTime / (UINT32_MAX$1 + 1)); 2713 const lower = Math.floor(baseMediaDecodeTime % (UINT32_MAX$1 + 1)); 2714 writeUint32(tfdt, 4, upper); 2715 writeUint32(tfdt, 8, lower); 2716 } 2717 } 2718 }); 2719 }); 2720 }); 2721} 2722 2723// TODO: Check if the last moof+mdat pair is part of the valid range 2724function segmentValidRange(data) { 2725 const segmentedRange = { 2726 valid: null, 2727 remainder: null 2728 }; 2729 const moofs = findBox(data, ['moof']); 2730 if (moofs.length < 2) { 2731 segmentedRange.remainder = data; 2732 return segmentedRange; 2733 } 2734 const last = moofs[moofs.length - 1]; 2735 // Offset by 8 bytes; findBox offsets the start by as much 2736 segmentedRange.valid = sliceUint8(data, 0, last.byteOffset - 8); 2737 segmentedRange.remainder = sliceUint8(data, last.byteOffset - 8); 2738 return segmentedRange; 2739} 2740function appendUint8Array(data1, data2) { 2741 const temp = new Uint8Array(data1.length + data2.length); 2742 temp.set(data1); 2743 temp.set(data2, data1.length); 2744 return temp; 2745} 2746function parseSamples(timeOffset, track) { 2747 const seiSamples = []; 2748 const videoData = track.samples; 2749 const timescale = track.timescale; 2750 const trackId = track.id; 2751 let isHEVCFlavor = false; 2752 const moofs = findBox(videoData, ['moof']); 2753 moofs.map(moof => { 2754 const moofOffset = moof.byteOffset - 8; 2755 const trafs = findBox(moof, ['traf']); 2756 trafs.map(traf => { 2757 // get the base media decode time from the tfdt 2758 const baseTime = findBox(traf, ['tfdt']).map(tfdt => { 2759 const version = tfdt[0]; 2760 let result = readUint32(tfdt, 4); 2761 if (version === 1) { 2762 result *= Math.pow(2, 32); 2763 result += readUint32(tfdt, 8); 2764 } 2765 return result / timescale; 2766 })[0]; 2767 if (baseTime !== undefined) { 2768 timeOffset = baseTime; 2769 } 2770 return findBox(traf, ['tfhd']).map(tfhd => { 2771 const id = readUint32(tfhd, 4); 2772 const tfhdFlags = readUint32(tfhd, 0) & 0xffffff; 2773 const baseDataOffsetPresent = (tfhdFlags & 0x000001) !== 0; 2774 const sampleDescriptionIndexPresent = (tfhdFlags & 0x000002) !== 0; 2775 const defaultSampleDurationPresent = (tfhdFlags & 0x000008) !== 0; 2776 let defaultSampleDuration = 0; 2777 const defaultSampleSizePresent = (tfhdFlags & 0x000010) !== 0; 2778 let defaultSampleSize = 0; 2779 const defaultSampleFlagsPresent = (tfhdFlags & 0x000020) !== 0; 2780 let tfhdOffset = 8; 2781 if (id === trackId) { 2782 if (baseDataOffsetPresent) { 2783 tfhdOffset += 8; 2784 } 2785 if (sampleDescriptionIndexPresent) { 2786 tfhdOffset += 4; 2787 } 2788 if (defaultSampleDurationPresent) { 2789 defaultSampleDuration = readUint32(tfhd, tfhdOffset); 2790 tfhdOffset += 4; 2791 } 2792 if (defaultSampleSizePresent) { 2793 defaultSampleSize = readUint32(tfhd, tfhdOffset); 2794 tfhdOffset += 4; 2795 } 2796 if (defaultSampleFlagsPresent) { 2797 tfhdOffset += 4; 2798 } 2799 if (track.type === 'video') { 2800 isHEVCFlavor = isHEVC(track.codec); 2801 } 2802 findBox(traf, ['trun']).map(trun => { 2803 const version = trun[0]; 2804 const flags = readUint32(trun, 0) & 0xffffff; 2805 const dataOffsetPresent = (flags & 0x000001) !== 0; 2806 let dataOffset = 0; 2807 const firstSampleFlagsPresent = (flags & 0x000004) !== 0; 2808 const sampleDurationPresent = (flags & 0x000100) !== 0; 2809 let sampleDuration = 0; 2810 const sampleSizePresent = (flags & 0x000200) !== 0; 2811 let sampleSize = 0; 2812 const sampleFlagsPresent = (flags & 0x000400) !== 0; 2813 const sampleCompositionOffsetsPresent = (flags & 0x000800) !== 0; 2814 let compositionOffset = 0; 2815 const sampleCount = readUint32(trun, 4); 2816 let trunOffset = 8; // past version, flags, and sample count 2817 2818 if (dataOffsetPresent) { 2819 dataOffset = readUint32(trun, trunOffset); 2820 trunOffset += 4; 2821 } 2822 if (firstSampleFlagsPresent) { 2823 trunOffset += 4; 2824 } 2825 let sampleOffset = dataOffset + moofOffset; 2826 for (let ix = 0;
vendor: 8,844 bytes, lines 2826-3110
2826 ix < sampleCount; ix++) { 2827 if (sampleDurationPresent) { 2828 sampleDuration = readUint32(trun, trunOffset); 2829 trunOffset += 4; 2830 } else { 2831 sampleDuration = defaultSampleDuration; 2832 } 2833 if (sampleSizePresent) { 2834 sampleSize = readUint32(trun, trunOffset); 2835 trunOffset += 4; 2836 } else { 2837 sampleSize = defaultSampleSize; 2838 } 2839 if (sampleFlagsPresent) { 2840 trunOffset += 4; 2841 } 2842 if (sampleCompositionOffsetsPresent) { 2843 if (version === 0) { 2844 compositionOffset = readUint32(trun, trunOffset); 2845 } else { 2846 compositionOffset = readSint32(trun, trunOffset); 2847 } 2848 trunOffset += 4; 2849 } 2850 if (track.type === ElementaryStreamTypes.VIDEO) { 2851 let naluTotalSize = 0; 2852 while (naluTotalSize < sampleSize) { 2853 const naluSize = readUint32(videoData, sampleOffset); 2854 sampleOffset += 4; 2855 if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) { 2856 const data = videoData.subarray(sampleOffset, sampleOffset + naluSize); 2857 parseSEIMessageFromNALu(data, isHEVCFlavor ? 2 : 1, timeOffset + compositionOffset / timescale, seiSamples); 2858 } 2859 sampleOffset += naluSize; 2860 naluTotalSize += naluSize + 4; 2861 } 2862 } 2863 timeOffset += sampleDuration / timescale; 2864 } 2865 }); 2866 } 2867 }); 2868 }); 2869 }); 2870 return seiSamples; 2871} 2872function isHEVC(codec) { 2873 if (!codec) { 2874 return false; 2875 } 2876 const delimit = codec.indexOf('.'); 2877 const baseCodec = delimit < 0 ? codec : codec.substring(0, delimit); 2878 return baseCodec === 'hvc1' || baseCodec === 'hev1' || 2879 // Dolby Vision 2880 baseCodec === 'dvh1' || baseCodec === 'dvhe'; 2881} 2882function isSEIMessage(isHEVCFlavor, naluHeader) { 2883 if (isHEVCFlavor) { 2884 const naluType = naluHeader >> 1 & 0x3f; 2885 return naluType === 39 || naluType === 40; 2886 } else { 2887 const naluType = naluHeader & 0x1f; 2888 return naluType === 6; 2889 } 2890} 2891function parseSEIMessageFromNALu(unescapedData, headerSize, pts, samples) { 2892 const data = discardEPB(unescapedData); 2893 let seiPtr = 0; 2894 // skip nal header 2895 seiPtr += headerSize; 2896 let payloadType = 0; 2897 let payloadSize = 0; 2898 let b = 0; 2899 while (seiPtr < data.length) { 2900 payloadType = 0; 2901 do { 2902 if (seiPtr >= data.length) { 2903 break; 2904 } 2905 b = data[seiPtr++]; 2906 payloadType += b; 2907 } while (b === 0xff); 2908 2909 // Parse payload size. 2910 payloadSize = 0; 2911 do { 2912 if (seiPtr >= data.length) { 2913 break; 2914 } 2915 b = data[seiPtr++]; 2916 payloadSize += b; 2917 } while (b === 0xff); 2918 const leftOver = data.length - seiPtr; 2919 // Create a variable to process the payload 2920 let payPtr = seiPtr; 2921 2922 // Increment the seiPtr to the end of the payload 2923 if (payloadSize < leftOver) { 2924 seiPtr += payloadSize; 2925 } else if (payloadSize > leftOver) { 2926 // Some type of corruption has happened? 2927 logger.error(`Malformed SEI payload. ${payloadSize} is too small, only ${leftOver} bytes left to parse.`); 2928 // We might be able to parse some data, but let's be safe and ignore it. 2929 break; 2930 } 2931 if (payloadType === 4) { 2932 const countryCode = data[payPtr++]; 2933 if (countryCode === 181) { 2934 const providerCode = readUint16(data, payPtr); 2935 payPtr += 2; 2936 if (providerCode === 49) { 2937 const userStructure = readUint32(data, payPtr); 2938 payPtr += 4; 2939 if (userStructure === 0x47413934) { 2940 const userDataType = data[payPtr++]; 2941 2942 // Raw CEA-608 bytes wrapped in CEA-708 packet 2943 if (userDataType === 3) { 2944 const firstByte = data[payPtr++]; 2945 const totalCCs = 0x1f & firstByte; 2946 const enabled = 0x40 & firstByte; 2947 const totalBytes = enabled ? 2 + totalCCs * 3 : 0; 2948 const byteArray = new Uint8Array(totalBytes); 2949 if (enabled) { 2950 byteArray[0] = firstByte; 2951 for (let i = 1; i < totalBytes; i++) { 2952 byteArray[i] = data[payPtr++]; 2953 } 2954 } 2955 samples.push({ 2956 type: userDataType, 2957 payloadType, 2958 pts, 2959 bytes: byteArray 2960 }); 2961 } 2962 } 2963 } 2964 } 2965 } else if (payloadType === 5) { 2966 if (payloadSize > 16) { 2967 const uuidStrArray = []; 2968 for (let i = 0; i < 16; i++) { 2969 const _b = data[payPtr++].toString(16); 2970 uuidStrArray.push(_b.length == 1 ? '0' + _b : _b); 2971 if (i === 3 || i === 5 || i === 7 || i === 9) { 2972 uuidStrArray.push('-'); 2973 } 2974 } 2975 const length = payloadSize - 16; 2976 const userDataBytes = new Uint8Array(length); 2977 for (let i = 0; i < length; i++) { 2978 userDataBytes[i] = data[payPtr++]; 2979 } 2980 samples.push({ 2981 payloadType, 2982 pts, 2983 uuid: uuidStrArray.join(''), 2984 userData: utf8ArrayToStr(userDataBytes), 2985 userDataBytes 2986 }); 2987 } 2988 } 2989 } 2990} 2991 2992/** 2993 * remove Emulation Prevention bytes from a RBSP 2994 */ 2995function discardEPB(data) { 2996 const length = data.byteLength; 2997 const EPBPositions = []; 2998 let i = 1; 2999 3000 // Find all `Emulation Prevention Bytes` 3001 while (i < length - 2) { 3002 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 3003 EPBPositions.push(i + 2); 3004 i += 2; 3005 } else { 3006 i++; 3007 } 3008 } 3009 3010 // If no Emulation Prevention Bytes were found just return the original 3011 // array 3012 if (EPBPositions.length === 0) { 3013 return data; 3014 } 3015 3016 // Create a new array to hold the NAL unit data 3017 const newLength = length - EPBPositions.length; 3018 const newData = new Uint8Array(newLength); 3019 let sourceIndex = 0; 3020 for (i = 0; i < newLength; sourceIndex++, i++) { 3021 if (sourceIndex === EPBPositions[0]) { 3022 // Skip this byte 3023 sourceIndex++; 3024 // Remove this position index 3025 EPBPositions.shift(); 3026 } 3027 newData[i] = data[sourceIndex]; 3028 } 3029 return newData; 3030} 3031function parseEmsg(data) { 3032 const version = data[0]; 3033 let schemeIdUri = ''; 3034 let value = ''; 3035 let timeScale = 0; 3036 let presentationTimeDelta = 0; 3037 let presentationTime = 0; 3038 let eventDuration = 0; 3039 let id = 0; 3040 let offset = 0; 3041 if (version === 0) { 3042 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 3043 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 3044 offset += 1; 3045 } 3046 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 3047 offset += 1; 3048 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 3049 value += bin2str(data.subarray(offset, offset + 1)); 3050 offset += 1; 3051 } 3052 value += bin2str(data.subarray(offset, offset + 1)); 3053 offset += 1; 3054 timeScale = readUint32(data, 12); 3055 presentationTimeDelta = readUint32(data, 16); 3056 eventDuration = readUint32(data, 20); 3057 id = readUint32(data, 24); 3058 offset = 28; 3059 } else if (version === 1) { 3060 offset += 4; 3061 timeScale = readUint32(data, offset); 3062 offset += 4; 3063 const leftPresentationTime = readUint32(data, offset); 3064 offset += 4; 3065 const rightPresentationTime = readUint32(data, offset); 3066 offset += 4; 3067 presentationTime = 2 ** 32 * leftPresentationTime + rightPresentationTime; 3068 if (!isSafeInteger(presentationTime)) { 3069 presentationTime = Number.MAX_SAFE_INTEGER; 3070 logger.warn('Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box'); 3071 } 3072 eventDuration = readUint32(data, offset); 3073 offset += 4; 3074 id = readUint32(data, offset); 3075 offset += 4; 3076 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 3077 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 3078 offset += 1; 3079 } 3080 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 3081 offset += 1; 3082 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 3083 value += bin2str(data.subarray(offset, offset + 1)); 3084 offset += 1; 3085 } 3086 value += bin2str(data.subarray(offset, offset + 1)); 3087 offset += 1; 3088 } 3089 const payload = data.subarray(offset, data.byteLength); 3090 return { 3091 schemeIdUri, 3092 value, 3093 timeScale, 3094 presentationTime, 3095 presentationTimeDelta, 3096 eventDuration, 3097 id, 3098 payload 3099 }; 3100} 3101function mp4Box(type, ...payload) { 3102 const len = payload.length; 3103 let size = 8; 3104 let i = len; 3105 while (i--) { 3106 size += payload[i].byteLength; 3107 } 3108 const result = new Uint8Array(size); 3109 result[0] = size >> 24 & 0xff; 3110 result[1] = size >> 16 & 0xff;
3111 result[2] = size >> 8 & 0xff; 3112 result[3] = size & 0xff; 3113 result.set(type, 4); 3114 for (i = 0, size = 8; i < len; i++) { 3115 result.set(payload[i], size); 3116 size += payload[i].byteLength; 3117 } 3118 return result; 3119} 3120function mp4pssh(systemId, keyids, data) { 3121 if (systemId.byteLength !== 16) { 3122 throw new RangeError('Invalid system id'); 3123 } 3124 let version; 3125 let kids; 3126 { 3127 version = 0; 3128 kids = new Uint8Array(); 3129 } 3130 let kidCount; 3131 if (version > 0) { 3132 kidCount = new Uint8Array(4); 3133 if (keyids.length > 0) { 3134 new DataView(kidCount.buffer).setUint32(0, keyids.length, false); 3135 } 3136 } else { 3137 kidCount = new Uint8Array(); 3138 } 3139 const dataSize = new Uint8Array(4); 3140 if (data && data.byteLength > 0) { 3141 new DataView(dataSize.buffer).setUint32(0, data.byteLength, false); 3142 } 3143 return mp4Box([112, 115, 115, 104], new Uint8Array([version, 0x00, 0x00, 0x00 // Flags 3144 ]), systemId, 3145 // 16 bytes 3146 kidCount, kids, dataSize, data || new Uint8Array()); 3147} 3148function parsePssh(initData) { 3149 if (!(initData instanceof ArrayBuffer) || initData.byteLength < 32) { 3150 return null; 3151 } 3152 const result = { 3153 version: 0, 3154 systemId: '', 3155 kids: null, 3156 data: null 3157 }; 3158 const view = new DataView(initData); 3159 const boxSize = view.getUint32(0); 3160 if (initData.byteLength !== boxSize && boxSize > 44) { 3161 return null; 3162 } 3163 const type = view.getUint32(4); 3164 if (type !== 0x70737368) { 3165 return null; 3166 } 3167 result.version = view.getUint32(8) >>> 24; 3168 if (result.version > 1) { 3169 return null; 3170 } 3171 result.systemId = Hex.hexDump(new Uint8Array(initData, 12, 16)); 3172 const dataSizeOrKidCount = view.getUint32(28); 3173 if (result.version === 0) { 3174 if (boxSize - 32 < dataSizeOrKidCount) { 3175 return null; 3176 } 3177 result.data = new Uint8Array(initData, 32, dataSizeOrKidCount); 3178 } else if (result.version === 1) { 3179 result.kids = []; 3180 for (let i = 0; i < dataSizeOrKidCount; i++) { 3181 result.kids.push(new Uint8Array(initData, 32 + i * 16, 16)); 3182 } 3183 } 3184 return result; 3185} 3186 3187let keyUriToKeyIdMap = {}; 3188class LevelKey { 3189 static clearKeyUriToKeyIdMap() { 3190 keyUriToKeyIdMap = {}; 3191 } 3192 constructor(method, uri, format, formatversions = [1], iv = null) { 3193 this.uri = void 0; 3194 this.method = void 0; 3195 this.keyFormat = void 0; 3196 this.keyFormatVersions = void 0; 3197 this.encrypted = void 0; 3198 this.isCommonEncryption = void 0; 3199 this.iv = null; 3200 this.key = null; 3201 this.keyId = null; 3202 this.pssh = null; 3203 this.method = method; 3204 this.uri = uri; 3205 this.keyFormat = format; 3206 this.keyFormatVersions = formatversions; 3207 this.iv = iv; 3208 this.encrypted = method ? method !== 'NONE' : false; 3209 this.isCommonEncryption = this.encrypted && method !== 'AES-128'; 3210 } 3211 isSupported() { 3212 // If it's Segment encryption or No encryption, just select that key system 3213 if (this.method) { 3214 if (this.method === 'AES-128' || this.method === 'NONE') { 3215 return true; 3216 } 3217 if (this.keyFormat === 'identity') { 3218 // Maintain support for clear SAMPLE-AES with MPEG-3 TS 3219 return this.method === 'SAMPLE-AES'; 3220 } else { 3221 switch (this.keyFormat) { 3222 case KeySystemFormats.FAIRPLAY: 3223 case KeySystemFormats.WIDEVINE: 3224 case KeySystemFormats.PLAYREADY:
3225 case KeySystemFormats.CLEARKEY: 3226 return ['ISO-23001-7', 'SAMPLE-AES', 'SAMPLE-AES-CENC', 'SAMPLE-AES-CTR'].indexOf(this.method) !== -1; 3227 } 3228 } 3229 } 3230 return false; 3231 } 3232 getDecryptData(sn) { 3233 if (!this.encrypted || !this.uri) { 3234 return null; 3235 } 3236 if (this.method === 'AES-128' && this.uri && !this.iv) { 3237 if (typeof sn !== 'number') { 3238 // We are fetching decryption data for a initialization segment 3239 // If the segment was encrypted with AES-128 3240 // It must have an IV defined. We cannot substitute the Segment Number in. 3241 if (this.method === 'AES-128' && !this.iv) { 3242 logger.warn(`missing IV for initialization segment with method="${this.method}" - compliance issue`); 3243 } 3244 // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. 3245 sn = 0; 3246 } 3247 const iv = createInitializationVector(sn); 3248 const decryptdata = new LevelKey(this.method, this.uri, 'identity', this.keyFormatVersions, iv); 3249 return decryptdata; 3250 } 3251 3252 // Initialize keyId if possible 3253 const keyBytes = convertDataUriToArrayBytes(this.uri); 3254 if (keyBytes) { 3255 switch (this.keyFormat) { 3256 case KeySystemFormats.WIDEVINE: 3257 this.pssh = keyBytes; 3258 // In case of widevine keyID is embedded in PSSH box. Read Key ID. 3259 if (keyBytes.length >= 22) { 3260 this.keyId = keyBytes.subarray(keyBytes.length - 22, keyBytes.length - 6); 3261 } 3262 break; 3263 case KeySystemFormats.PLAYREADY: 3264 { 3265 const PlayReadyKeySystemUUID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]); 3266 this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes); 3267 const keyBytesUtf16 = new Uint16Array(keyBytes.buffer, keyBytes.byteOffset, keyBytes.byteLength / 2); 3268 const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16)); 3269 3270 // Parse Playready WRMHeader XML 3271 const xmlKeyBytes = keyByteStr.substring(keyByteStr.indexOf('<'), keyByteStr.length); 3272 const parser = new DOMParser(); 3273 const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml'); 3274 const keyData = xmlDoc.getElementsByTagName('KID')[0]; 3275 if (keyData) { 3276 const keyId = keyData.childNodes[0] ? keyData.childNodes[0].nodeValue : keyData.getAttribute('VALUE'); 3277 if (keyId) { 3278 const keyIdArray = base64Decode(keyId).subarray(0, 16); 3279 // KID value in PRO is a base64-encoded little endian GUID interpretation of UUID 3280 // KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID 3281 changeEndianness(keyIdArray); 3282 this.keyId = keyIdArray; 3283 } 3284 } 3285 break; 3286 } 3287 default: 3288 { 3289 let keydata = keyBytes.subarray(0, 16); 3290 if (keydata.length !== 16) { 3291 const padded = new Uint8Array(16); 3292 padded.set(keydata, 16 - keydata.length); 3293 keydata = padded; 3294 } 3295 this.keyId = keydata; 3296 break; 3297 } 3298 } 3299 } 3300 3301 // Default behavior: assign a new keyId for each uri 3302 if (!this.keyId || this.keyId.byteLength !== 16) { 3303 let keyId = keyUriToKeyIdMap[this.uri]; 3304 if (!keyId) { 3305 const val = Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER; 3306 keyId = new Uint8Array(16); 3307 const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes 3308 dv.setUint32(0, val); 3309 keyUriToKeyIdMap[this.uri] = keyId; 3310 } 3311 this.keyId = keyId; 3312 } 3313 return this; 3314 } 3315} 3316function createInitializationVector(segmentNumber) { 3317 const uint8View = new Uint8Array(16); 3318 for (let i = 12; i < 16; i++) { 3319 uint8View[i] = segmentNumber >> 8 * (15 - i) & 0xff; 3320 } 3321 return uint8View; 3322} 3323 3324const VARIABLE_REPLACEMENT_REGEX = /\{\$([a-zA-Z0-9-_]+)\}/g; 3325function hasVariableReferences(str) { 3326 return VARIABLE_REPLACEMENT_REGEX.test(str); 3327} 3328function substituteVariablesInAttributes(parsed, attr, attributeNames) { 3329 if (parsed.variableList !== null || parsed.hasVariableRefs) { 3330 for (let i = attributeNames.length; i--;) { 3331 const name = attributeNames[i]; 3332 const value = attr[name]; 3333 if (value) { 3334 attr[name] = substituteVariables(parsed, value); 3335 } 3336 } 3337 } 3338} 3339function substituteVariables(parsed, value) { 3340 if (parsed.variableList !== null || parsed.hasVariableRefs) { 3341 const variableList = parsed.variableList; 3342 return value.replace(VARIABLE_REPLACEMENT_REGEX, variableReference => { 3343 const variableName = variableReference.substring(2, variableReference.length - 1); 3344 const variableValue = variableList == null ? void 0 : variableList[variableName]; 3345 if (variableValue === undefined) { 3346 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`Missing preceding EXT-X-DEFINE tag for Variable Reference: "${variableName}"`)); 3347 return variableReference; 3348 } 3349 return variableValue; 3350 }); 3351 } 3352 return value; 3353} 3354function addVariableDefinition(parsed, attr, parentUrl) { 3355 let variableList = parsed.variableList; 3356 if (!variableList) { 3357 parsed.variableList = variableList = {}; 3358 } 3359 let NAME; 3360 let VALUE; 3361 if ('QUERYPARAM' in attr) { 3362 NAME = attr.QUERYPARAM; 3363 try { 3364 const searchParams = new self.URL(parentUrl).searchParams; 3365 if (searchParams.has(NAME)) { 3366 VALUE = searchParams.get(NAME); 3367 } else { 3368 throw new Error(`"${NAME}" does not match any query parameter in URI: "${parentUrl}"`); 3369 } 3370 } catch (error) { 3371 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE QUERYPARAM: ${error.message}`)); 3372 } 3373 } else { 3374 NAME = attr.NAME; 3375 VALUE = attr.VALUE; 3376 } 3377 if (NAME in variableList) { 3378 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE duplicate Variable Name declarations: "${NAME}"`)); 3379 } else { 3380 variableList[NAME] = VALUE || ''; 3381 } 3382} 3383function importVariableDefinition(parsed, attr, sourceVariableList) { 3384 const IMPORT = attr.IMPORT; 3385 if (sourceVariableList && IMPORT in sourceVariableList) { 3386 let variableList = parsed.variableList; 3387 if (!variableList) { 3388 parsed.variableList = variableList = {}; 3389 } 3390 variableList[IMPORT] = sourceVariableList[IMPORT]; 3391 } else { 3392 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "${IMPORT}"`)); 3393 } 3394} 3395 3396/** 3397 * MediaSource helper 3398 */ 3399 3400function getMediaSource(preferManagedMediaSource = true) { 3401 if (typeof self === 'undefined') return undefined; 3402 const mms = (preferManagedMediaSource || !self.MediaSource) && self.ManagedMediaSource; 3403 return mms || self.MediaSource || self.WebKitMediaSource; 3404} 3405function isManagedMediaSource(source) { 3406 return typeof self !== 'undefined' && source === self.ManagedMediaSource; 3407} 3408 3409// from http://mp4ra.org/codecs.html 3410// values indicate codec selection preference (lower is higher priority) 3411const sampleEntryCodesISO = { 3412 audio: { 3413 a3ds: 1, 3414 'ac-3': 0.95, 3415 'ac-4': 1, 3416 alac: 0.9, 3417 alaw: 1, 3418 dra1: 1, 3419 'dts+': 1, 3420 'dts-': 1, 3421 dtsc: 1, 3422 dtse: 1, 3423 dtsh: 1, 3424 'ec-3': 0.9, 3425 enca: 1, 3426 fLaC: 0.9, 3427 // MP4-RA listed codec entry for FLAC 3428 flac: 0.9, 3429 // legacy browser codec name for FLAC 3430 FLAC: 0.9, 3431 // some manifests may list "FLAC" with Apple's tools 3432 g719: 1, 3433 g726: 1, 3434 m4ae: 1, 3435 mha1: 1, 3436 mha2: 1, 3437 mhm1: 1, 3438 mhm2: 1, 3439 mlpa: 1, 3440 mp4a: 1, 3441 'raw ': 1, 3442 Opus: 1, 3443 opus: 1, 3444 // browsers expect this to be lowercase despite MP4RA says 'Opu
3444s' 3445 samr: 1, 3446 sawb: 1, 3447 sawp: 1, 3448 sevc: 1, 3449 sqcp: 1, 3450 ssmv: 1, 3451 twos: 1, 3452 ulaw: 1 3453 }, 3454 video: { 3455 avc1: 1, 3456 avc2: 1, 3457 avc3: 1, 3458 avc4: 1, 3459 avcp: 1, 3460 av01: 0.8, 3461 drac: 1, 3462 dva1: 1, 3463 dvav: 1, 3464 dvh1: 0.7, 3465 dvhe: 0.7, 3466 encv: 1, 3467 hev1: 0.75, 3468 hvc1: 0.75, 3469 mjp2: 1, 3470 mp4v: 1, 3471 mvc1: 1, 3472 mvc2: 1, 3473 mvc3: 1, 3474 mvc4: 1, 3475 resv: 1, 3476 rv60: 1, 3477 s263: 1, 3478 svc1: 1, 3479 svc2: 1, 3480 'vc-1': 1, 3481 vp08: 1, 3482 vp09: 0.9 3483 }, 3484 text: { 3485 stpp: 1, 3486 wvtt: 1 3487 } 3488}; 3489function isCodecType(codec, type) { 3490 const typeCodes = sampleEntryCodesISO[type]; 3491 return !!typeCodes && !!typeCodes[codec.slice(0, 4)]; 3492} 3493function areCodecsMediaSourceSupported(codecs, type, preferManagedMediaSource = true) { 3494 return !codecs.split(',').some(codec => !isCodecMediaSourceSupported(codec, type, preferManagedMediaSource)); 3495} 3496function isCodecMediaSourceSupported(codec, type, preferManagedMediaSource = true) { 3497 var _MediaSource$isTypeSu; 3498 const MediaSource = getMediaSource(preferManagedMediaSource); 3499 return (_MediaSource$isTypeSu = MediaSource == null ? void 0 : MediaSource.isTypeSupported(mimeTypeForCodec(codec, type))) != null ? _MediaSource$isTypeSu : false;
3500} 3501function mimeTypeForCodec(codec, type) { 3502 return `${type}/mp4;codecs="${codec}"`; 3503} 3504function videoCodecPreferenceValue(videoCodec) { 3505 if (videoCodec) { 3506 const fourCC = videoCodec.substring(0, 4); 3507 return sampleEntryCodesISO.video[fourCC]; 3508 } 3509 return 2; 3510} 3511function codecsSetSelectionPreferenceValue(codecSet) { 3512 return codecSet.split(',').reduce((num, fourCC) => { 3513 const preferenceValue = sampleEntryCodesISO.video[fourCC]; 3514 if (preferenceValue) { 3515 return (preferenceValue * 2 + num) / (num ? 3 : 2); 3516 } 3517 return (sampleEntryCodesISO.audio[fourCC] + num) / (num ? 2 : 1); 3518 }, 0); 3519} 3520const CODEC_COMPATIBLE_NAMES = {}; 3521function getCodecCompatibleNameLower(lowerCaseCodec, preferManagedMediaSource = true) { 3522 if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) { 3523 return CODEC_COMPATIBLE_NAMES[lowerCaseCodec]; 3524 } 3525 3526 // Idealy fLaC and Opus would be first (spec-compliant) but 3527 // some browsers will report that fLaC is supported then fail. 3528 // see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728 3529 const codecsToCheck = { 3530 flac: ['flac', 'fLaC', 'FLAC'], 3531 opus: ['opus', 'Opus'] 3532 }[lowerCaseCodec]; 3533 for (let i = 0; i < codecsToCheck.length; i++) { 3534 if (isCodecMediaSourceSupported(codecsToCheck[i], 'audio', preferManagedMediaSource)) { 3535 CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i]; 3536 return codecsToCheck[i]; 3537 } 3538 } 3539 return lowerCaseCodec; 3540} 3541const AUDIO_CODEC_REGEXP = /flac|opus/i; 3542function getCodecCompatibleName(codec, preferManagedMediaSource = true) { 3543 return codec.replace(AUDIO_CODEC_REGEXP, m => getCodecCompatibleNameLower(m.toLowerCase(), preferManagedMediaSource)); 3544} 3545function pickMostCompleteCodecName(parsedCodec, levelCodec) { 3546 // Parsing of mp4a codecs strings in mp4-tools from media is incomplete as of d8c6c7a 3547 // so use level codec is parsed codec is unavailable or incomplete 3548 if (parsedCodec && parsedCodec !== 'mp4a') { 3549 return parsedCodec; 3550 } 3551 return levelCodec ? levelCodec.split(',')[0] : levelCodec; 3552} 3553function convertAVC1ToAVCOTI(codec) { 3554 // Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported 3555 // Examples: avc1.66.30 to avc1.42001e and avc1.77.30,avc1.66.30 to avc1.4d001e,avc1.42001e. 3556 const codecs = codec.split(','); 3557 for (let i = 0; i < codecs.length; i++) { 3558 const avcdata = codecs[i].split('.'); 3559 if (avcdata.length > 2) { 3560 let result = avcdata.shift() + '.'; 3561 result += parseInt(avcdata.shift()).toString(16); 3562 result += ('000' + parseInt(avcdata.shift()).toString(16)).slice(-4); 3563 codecs[i] = result; 3564 } 3565 } 3566 return codecs.join(','); 3567} 3568 3569const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g; 3570const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g; 3571const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present) 3572 3573const LEVEL_PLAYLIST_REGEX_FAST = new RegExp([/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, 3574// duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title 3575/(?!#) *(\S[^\r\n]*)/.source, 3576// segment URI, group 3 => the URI (note newline is not eaten) 3577/#EXT-X-BYTERANGE:*(.+)/.source, 3578// next segment's byterange, group 4 => range spec (x@y) 3579/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, 3580// next segment's program date/time group 5 => the datetime spec 3581/#.*/.source // All other non-segment oriented tags will match with all groups empty 3582].join('|'), 'g'); 3583const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp([/#(EXTM3U)/.source, /#EXT-X-(DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/.source, /#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/.source, /#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source, /(#)([^:]*):(.*)/.source, /(#)(.*)(?:.*)\r?\n?/.source].join('|')); 3584class M3U8Parser { 3585 static findGroup(groups, mediaGroupId) { 3586 for (let i = 0; i < groups.length; i++) { 3587 const group = groups[i]; 3588 if (group.id === mediaGroupId) { 3589 return group; 3590 } 3591 } 3592 } 3593 static resolve(url, baseUrl) { 3594 return urlToolkitExports.buildAbsoluteURL(baseUrl, url, { 3595 alwaysNormalize: true 3596 }); 3597 } 3598 static isMediaPlaylist(str) { 3599 return IS_MEDIA_PLAYLIST.test(str); 3600 } 3601 static parseMasterPlaylist(string, baseurl) { 3602 const hasVariableRefs = hasVariableReferences(string) ; 3603 const parsed = { 3604 contentSteering: null, 3605 levels: [], 3606 playlistParsingError: null, 3607 sessionData: null, 3608 sessionKeys: null, 3609 startTimeOffset: null, 3610 variableList: null, 3611 hasVariableRefs 3612 }; 3613 const levelsWithKnownCodecs = []; 3614 MASTER_PLAYLIST_REGEX.lastIndex = 0; 3615 let result; 3616 while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) { 3617 if (result[1]) { 3618 var _level$unknownCodecs; 3619 // '#EXT-X-STREAM-INF' is found, parse level tag in group 1 3620 const attrs = new AttrList(result[1]); 3621 { 3622 substituteVariablesInAttributes(parsed, attrs, ['CODECS', 'SUPPLEMENTAL-CODECS', 'ALLOWED-CPC', 'PATHWAY-ID', 'STABLE-VARIANT-ID', 'AUDIO', 'VIDEO', 'SUBTITLES', 'CLOSED-CAPTIONS', 'NAME']); 3623 } 3624 const uri = substituteVariables(parsed, result[2]) ; 3625 const level = { 3626 attrs, 3627 bitrate: attrs.decimalInteger('BANDWIDTH') || attrs.decimalInteger('AVERAGE-BANDWIDTH'), 3628 name: attrs.NAME, 3629 url: M3U8Parser.resolve(uri, baseurl) 3630 }; 3631 const resolution = attrs.decimalResolution('RESOLUTION'); 3632 if (resolution) { 3633 level.width = resolution.width; 3634 level.height = resolution.height; 3635 } 3636 setCodecs(attrs.CODECS, level); 3637 if (!((_level$unknownCodecs = level.unknownCodecs) != null && _level$unknownCodecs.length)) { 3638 levelsWithKnownCodecs.push(level); 3639 } 3640 parsed.levels.push(level); 3641 } else if (result[3]) { 3642 const tag = result[3]; 3643 const attributes = result[4]; 3644 switch (tag) { 3645 case 'SESSION-DATA': 3646 { 3647 // #EXT-X-SESSION-DATA 3648 const sessionAttrs = new AttrList(attributes); 3649 { 3650 substituteVariablesInAttributes(parsed, sessionAttrs, ['DATA-ID', 'LANGUAGE', 'VALUE', 'URI']); 3651 } 3652 const dataId = sessionAttrs['DATA-ID']; 3653 if (dataId) { 3654 if (parsed.sessionData === null) { 3655 parsed.sessionData = {}; 3656 } 3657 parsed.sessionData[dataId] = sessionAttrs; 3658 } 3659 break; 3660 } 3661 case 'SESSION-KEY': 3662 { 3663 // #EXT-X-SESSION-KEY 3664 const sessionKey = parseKey(attributes, baseurl, parsed); 3665 if (sessionKey.encrypted && sessionKey.isSupported()) { 3666 if (parsed.sessionKeys === null) { 3667 parsed.sessionKeys = []; 3668 } 3669 parsed.sessionKeys.push(sessionKey); 3670 } else { 3671 logger.warn(`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`); 3672 } 3673 break; 3674 } 3675 case 'DEFINE': 3676 { 3677 // #EXT-X-DEFINE 3678 { 3679 const variableAttributes = new AttrList(attributes); 3680 substituteVariablesInAttributes(parsed, variableAttributes, ['NAME', 'VALUE', 'QUERYPARAM']); 3681 addVariableDefinition(parsed, variableAttributes, baseurl); 3682 } 3683 break; 3684 } 3685 case 'CONTENT-STEERING': 3686 { 3687 // #EXT-X-CONTENT-STEERING 3688 const contentSteeringAttributes = new AttrList(attributes); 3689 { 3690 substituteVariablesInAttributes(parsed, contentSteeringAttributes, ['SERVER-URI', 'PATHWAY-ID']); 3691 } 3692 parsed.contentSteering = { 3693 uri: M3U8Parser.resolve(contentSteeringAttributes['SERVER-URI'], baseurl), 3694 pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.' 3695 }; 3696 break; 3697 } 3698 case 'START': 3699 { 3700 // #EXT-X-START 3701 parsed.startTimeOffset = parseStartTimeOffset(attributes); 3702 break; 3703 } 3704 } 3705 } 3706 } 3707 // Filter out levels with unknown codecs if it does not remove all levels 3708 const stripUnknownCodecLevels = levelsWithKnownCodecs.length > 0 && levelsWithKnownCodecs.length < parsed.levels.length; 3709 parsed.levels = stripUnknownCodecLevels ? levelsWithKnownCodecs : parsed.levels; 3710 if (parsed.levels.length === 0) { 3711 parsed.playlistParsingError = new Error('no levels found in manifest'); 3712 } 3713 return parsed; 3714 } 3715 static parseMasterPlaylistMedia(string, baseurl, parsed) { 3716 let result; 3717 const results = {}; 3718 const levels = parsed.levels; 3719 const groupsByType = { 3720 AUDIO: levels.map(level => ({ 3721 id: level.attrs.AUDIO, 3722 audioCodec: level.audioCodec 3723 })), 3724 SUBTITLES: levels.map(level => ({ 3725 id: level.attrs.SUBTITLES, 3726 textCodec: level.textCodec 3727 })), 3728 'CLOSED-CAPTIONS': [] 3729 }; 3730 let id = 0; 3731 MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0; 3732 while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) { 3733 const attrs = new AttrList(result[1]); 3734 const type = attrs.TYPE; 3735 if (type) { 3736 const groups = groupsByType[type]; 3737 const medias = results[type] || []; 3738 results[type] = medias; 3739 { 3740 substituteVariablesInAttributes(parsed, attrs, ['URI', 'GROUP-ID', 'LANGUAGE', 'ASSOC-LANGUAGE', 'STABLE-RENDITION-ID', 'NAME', 'INSTREAM-ID', 'CHARACTERISTICS', 'CHANNELS']); 3741 } 3742 const lang = attrs.LANGUAGE; 3743 const assocLang = attrs['ASSOC-LANGUAGE']; 3744 const channels = attrs.CHANNELS; 3745 const characteristics = attrs.CHARACTERISTICS; 3746 const instreamId = attrs['INSTREAM-ID']; 3747 const media = { 3748 attrs, 3749 bitrate: 0, 3750 id: id++, 3751 groupId: attrs['GROUP-ID'] || '', 3752 name: attrs.NAME || lang || '', 3753 type, 3754 default: attrs.bool('DEFAULT'), 3755 autoselect: attrs.bool('AUTOSELECT'), 3756 forced: attrs.bool('FORCED'), 3757 lang, 3758 url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '' 3759 }; 3760 if (assocLang) { 3761 media.assocLang = assocLang; 3762 } 3763 if (channels) { 3764 media.channels = channels; 3765 } 3766 if (characteristics) { 3767 media.characteristics = characteristics; 3768 } 3769 if (instreamId) { 3770 media.instreamId = instreamId; 3771 } 3772 if (groups != null && groups.length) {
3773 // If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track 3774 // If we don't find the track signalled, lets use the first audio groups codec we have 3775 // Acting as a best guess 3776 const groupCodec = M3U8Parser.findGroup(groups, media.groupId) || groups[0]; 3777 assignCodec(media, groupCodec, 'audioCodec'); 3778 assignCodec(media, groupCodec, 'textCodec'); 3779 } 3780 medias.push(media); 3781 } 3782 } 3783 return results; 3784 } 3785 static parseLevelPlaylist(string, baseurl, id, type, levelUrlId, multivariantVariableList) { 3786 const level = new LevelDetails(baseurl); 3787 const fragments = level.fragments; 3788 // The most recent init segment seen (applies to all subsequent segments) 3789 let currentInitSegment = null; 3790 let currentSN = 0; 3791 let currentPart = 0; 3792 let totalduration = 0; 3793 let discontinuityCounter = 0; 3794 let prevFrag = null; 3795 let frag = new Fragment(type, baseurl); 3796 let result; 3797 let i; 3798 let levelkeys; 3799 let firstPdtIndex = -1; 3800 let createNextFrag = false; 3801 let nextByteRange = null; 3802 LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0; 3803 level.m3u8 = string; 3804 level.hasVariableRefs = hasVariableReferences(string) ; 3805 while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) { 3806 if (createNextFrag) { 3807 createNextFrag = false; 3808 frag = new Fragment(type, baseurl); 3809 // setup the next fragment for part loading 3810 frag.start = totalduration; 3811 frag.sn = currentSN; 3812 frag.cc = discontinuityCounter; 3813 frag.level = id; 3814 if (currentInitSegment) { 3815 frag.initSegment = currentInitSegment; 3816 frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; 3817 currentInitSegment.rawProgramDateTime = null; 3818 if (nextByteRange) { 3819 frag.setByteRange(nextByteRange); 3820 nextByteRange = null; 3821 } 3822 } 3823 } 3824 const duration = result[1]; 3825 if (duration) { 3826 // INF 3827 frag.duration = parseFloat(duration); 3828 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 3829 const title = (' ' + result[2]).slice(1); 3830 frag.title = title || null; 3831 frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]); 3832 } else if (result[3]) { 3833 // url 3834 if (isFiniteNumber(frag.duration)) { 3835 frag.start = totalduration; 3836 if (levelkeys) { 3837 setFragLevelKeys(frag, levelkeys, level); 3838 } 3839 frag.sn = currentSN; 3840 frag.level = id; 3841 frag.cc = discontinuityCounter; 3842 fragments.push(frag); 3843 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 3844 const uri = (' ' + result[3]).slice(1); 3845 frag.relurl = substituteVariables(level, uri) ; 3846 assignProgramDateTime(frag, prevFrag); 3847 prevFrag = frag; 3848 totalduration += frag.duration; 3849 currentSN++; 3850 currentPart = 0; 3851 createNextFrag = true; 3852 } 3853 } else if (result[4]) { 3854 // X-BYTERANGE 3855 const data = (' ' + result[4]).slice(1); 3856 if (prevFrag) { 3857 frag.setByteRange(data, prevFrag); 3858 } else { 3859 frag.setByteRange(data); 3860 } 3861 } else if (result[5]) { 3862 // PROGRAM-DATE-TIME 3863 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 3864 frag.rawProgramDateTime = (' ' + result[5]).slice(1); 3865 frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]); 3866 if (firstPdtIndex === -1) { 3867 firstPdtIndex = fragments.length; 3868 } 3869 } else { 3870 result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW); 3871 if (!result) { 3872 logger.warn('No matches on slow regex match for level playlist!'); 3873 continue; 3874 } 3875 for (i = 1; i < result.length; i++) { 3876 if (typeof result[i] !== 'undefined') { 3877 break; 3878 } 3879 } 3880 3881 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 3882 const tag = (' ' + result[i]).slice(1); 3883 const value1 = (' ' + result[i + 1]).slice(1); 3884 const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : ''; 3885 switch (tag) { 3886 case 'PLAYLIST-TYPE': 3887 level.type = value1.toUpperCase(); 3888 break; 3889 case 'MEDIA-SEQUENCE': 3890 currentSN = level.startSN = parseInt(value1); 3891 break; 3892 case 'SKIP': 3893 { 3894 const skipAttrs = new AttrList(value1); 3895 { 3896 substituteVariablesInAttributes(level, skipAttrs, ['RECENTLY-REMOVED-DATERANGES']); 3897 } 3898 const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS'); 3899 if (isFiniteNumber(skippedSegments)) { 3900 level.skippedSegments = skippedSegments; 3901 // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails` 3902 for (let _i = skippedSegments; _i--;) { 3903 fragments.unshift(null); 3904 } 3905 currentSN += skippedSegments; 3906 } 3907 const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES'); 3908 if (recentlyRemovedDateranges) { 3909 level.recentlyRemovedDateranges = recentlyRemovedDateranges.split('\t'); 3910 } 3911 break; 3912 } 3913 case 'TARGETDURATION': 3914 level.targetduration = Math.max(parseInt(value1), 1); 3915 break; 3916 case 'VERSION':
3917 level.version = parseInt(value1); 3918 break; 3919 case 'INDEPENDENT-SEGMENTS': 3920 case 'EXTM3U': 3921 break; 3922 case 'ENDLIST': 3923 level.live = false; 3924 break; 3925 case '#': 3926 if (value1 || value2) { 3927 frag.tagList.push(value2 ? [value1, value2] : [value1]); 3928 } 3929 break; 3930 case 'DISCONTINUITY': 3931 discontinuityCounter++; 3932 frag.tagList.push(['DIS']); 3933 break; 3934 case 'GAP': 3935 frag.gap = true; 3936 frag.tagList.push([tag]); 3937 break; 3938 case 'BITRATE': 3939 frag.tagList.push([tag, value1]); 3940 break; 3941 case 'DATERANGE': 3942 { 3943 const dateRangeAttr = new AttrList(value1); 3944 { 3945 substituteVariablesInAttributes(level, dateRangeAttr, ['ID', 'CLASS', 'START-DATE', 'END-DATE', 'SCTE35-CMD', 'SCTE35-OUT', 'SCTE35-IN']); 3946 substituteVariablesInAttributes(level, dateRangeAttr, dateRangeAttr.clientAttrs); 3947 } 3948 const dateRange = new DateRange(dateRangeAttr, level.dateRanges[dateRangeAttr.ID]); 3949 if (dateRange.isValid || level.skippedSegments) { 3950 level.dateRanges[dateRange.id] = dateRange; 3951 } else { 3952 logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`); 3953 } 3954 // Add to fragment tag list for backwards compatibility (< v1.2.0) 3955 frag.tagList.push(['EXT-X-DATERANGE', value1]); 3956 break; 3957 } 3958 case 'DEFINE': 3959 { 3960 { 3961 const variableAttributes = new AttrList(value1); 3962 substituteVariablesInAttributes(level, variableAttributes, ['NAME', 'VALUE', 'IMPORT', 'QUERYPARAM']); 3963 if ('IMPORT' in variableAttributes) { 3964 importVariableDefinition(level, variableAttributes, multivariantVariableList); 3965 } else { 3966 addVariableDefinition(level, variableAttributes, baseurl); 3967 } 3968 } 3969 break; 3970 } 3971 case 'DISCONTINUITY-SEQUENCE': 3972 discontinuityCounter = parseInt(value1); 3973 break; 3974 case 'KEY': 3975 { 3976 const levelKey = parseKey(value1, baseurl, level); 3977 if (levelKey.isSupported()) { 3978 if (levelKey.method === 'NONE') { 3979 levelkeys = undefined; 3980 break; 3981 } 3982 if (!levelkeys) { 3983 levelkeys = {}; 3984 } 3985 if (levelkeys[levelKey.keyFormat]) { 3986 levelkeys = _extends({}, levelkeys); 3987 } 3988 levelkeys[levelKey.keyFormat] = levelKey; 3989 } else { 3990 logger.warn(`[Keys] Ignoring invalid EXT-X-KEY tag: "${value1}"`); 3991 } 3992 break; 3993 } 3994 case 'START': 3995 level.startTimeOffset = parseStartTimeOffset(value1); 3996 break; 3997 case 'MAP': 3998 { 3999 const mapAttrs = new AttrList(value1); 4000 { 4001 substituteVariablesInAttributes(level, mapAttrs, ['BYTERANGE', 'URI']); 4002 } 4003 if (frag.duration) { 4004 // Initial segment tag is after segment duration tag. 4005 // #EXTINF: 6.0 4006 // #EXT-X-MAP:URI="init.mp4 4007 const init = new Fragment(type, baseurl); 4008 setInitSegment(init, mapAttrs, id, levelkeys); 4009 currentInitSegment = init; 4010 frag.initSegment = currentInitSegment; 4011 if (currentInitSegment.rawProgramDateTime && !frag.rawProgramDateTime) { 4012 frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; 4013 } 4014 } else { 4015 // Initial segment tag is before segment duration tag 4016 // Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE 4017 const end = frag.byteRangeEndOffset; 4018 if (end) { 4019 const start = frag.byteRangeStartOffset; 4020 nextByteRange = `${end - start}@${start}`; 4021 } else { 4022 nextByteRange = null; 4023 } 4024 setInitSegment(frag, mapAttrs, id, levelkeys); 4025 currentInitSegment = frag; 4026 createNextFrag = true; 4027 } 4028 break; 4029 } 4030 case 'SERVER-CONTROL': 4031 { 4032 const serverControlAttrs = new AttrList(value1); 4033 level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD'); 4034 level.canSkipUntil = serverControlAttrs.optionalFloat('CAN-SKIP-UNTIL', 0); 4035 level.canSkipDateRanges = level.canSkipUntil > 0 && serverControlAttrs.bool('CAN-SKIP-DATERANGES'); 4036 level.partHoldBack = serverControlAttrs.optionalFloat('PART-HOLD-BACK', 0); 4037 level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0); 4038 break; 4039 } 4040 case 'PART-INF': 4041 { 4042 const partInfAttrs = new AttrList(value1); 4043 level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET'); 4044 break; 4045 } 4046 case 'PART': 4047 { 4048 let partList = level.partList; 4049 if (!partList) { 4050 partList = level.partList = []; 4051 } 4052 const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined; 4053 const index = currentPart++; 4054 const partAttrs = new AttrList(value1); 4055 { 4056 substituteVariablesInAttributes(level, partAttrs, ['BYTERANGE', 'URI']); 4057 } 4058 const part = new Part(partAttrs, frag, baseurl, index, previousFragmentPart); 4059 partList.push(part); 4060 frag.duration += part.duration; 4061 break; 4062 } 4063 case 'PRELOAD-HINT': 4064 { 4065 const preloadHintAttrs = new AttrList(value1); 4066 { 4067 substituteVariablesInAttributes(level, preloadHintAttrs, ['URI']); 4068 } 4069 level.preloadHint = preloadHintAttrs; 4070 break; 4071 } 4072 case 'RENDITION-REPORT': 4073 { 4074 const renditionReportAttrs = new AttrList(value1); 4075 { 4076 substituteVariablesInAttributes(level, renditionReportAttrs, ['URI']); 4077 } 4078 level.renditionReports = level.renditionReports || []; 4079 level.renditionReports.push(renditionReportAttrs); 4080 break; 4081 } 4082 default: 4083 logger.warn(`line parsed but not handled: ${result}`); 4084 break; 4085 } 4086 } 4087 } 4088 if (prevFrag && !prevFrag.relurl) { 4089 fragments.pop(); 4090 totalduration -= prevFrag.duration; 4091 if (level.partList) { 4092 level.fragmentHint = prevFrag; 4093 } 4094 } else if (level.partList) { 4095 assignProgramDateTime(frag, prevFrag); 4096 frag.cc = discontinuityCounter; 4097 level.fragmentHint = frag; 4098 if (levelkeys) { 4099 setFragLevelKeys(frag, levelkeys, level); 4100 } 4101 } 4102 const fragmentLength = fragments.length; 4103 const firstFragment = fragments[0]; 4104 const lastFragment = fragments[fragmentLength - 1]; 4105 totalduration += level.skippedSegments * level.targetduration; 4106 if (totalduration > 0 && fragmentLength && lastFragment) { 4107 level.averagetargetduration = totalduration / fragmentLength; 4108 const lastSn = lastFragment.sn; 4109 level.endSN = lastSn !== 'initSegment' ? lastSn : 0; 4110 if (!level.live) { 4111 lastFragment.endList = true; 4112 } 4113 if (firstFragment) { 4114 level.startCC = firstFragment.cc; 4115 } 4116 } else { 4117 level.endSN = 0; 4118 level.startCC = 0; 4119 } 4120 if (level.fragmentHint) { 4121 totalduration += level.fragmentHint.duration; 4122 } 4123 level.totalduration = totalduration; 4124 level.endCC = discontinuityCounter; 4125 4126 /** 4127 * Backfill any missing PDT values 4128 * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after 4129 * one or more Media Segment URIs, the client SHOULD extrapolate 4130 * backward from that tag (using EXTINF durations and/or media 4131 * timestamps) to associate dates with those segments." 4132 * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs 4133 * computed. 4134 */ 4135 if (firstPdtIndex > 0) { 4136 backfillProgramDateTimes(fragments, firstPdtIndex); 4137 } 4138 return level; 4139 } 4140} 4141function parseKey(keyTagAttributes, baseurl, parsed) { 4142 var _keyAttrs$METHOD, _keyAttrs$KEYFORMAT; 4143 // https://tools.ietf.org/html/rfc8216#section-4.3.2.4 4144 const keyAttrs = new AttrList(keyTagAttributes); 4145 { 4146 substituteVariablesInAttributes(parsed, keyAttrs, ['KEYFORMAT', 'KEYFORMATVERSIONS', 'URI', 'IV', 'URI']); 4147 } 4148 const decryptmethod = (_keyAttrs$METHOD = keyAttrs.METHOD) != null ? _keyAttrs$METHOD : ''; 4149 const decrypturi = keyAttrs.URI; 4150 const decryptiv = keyAttrs.hexadecimalInteger('IV'); 4151 const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS;
vendor: 7,470 bytes, lines 4152-4409
4152 // From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity". 4153 const decryptkeyformat = (_keyAttrs$KEYFORMAT = keyAttrs.KEYFORMAT) != null ? _keyAttrs$KEYFORMAT : 'identity'; 4154 if (decrypturi && keyAttrs.IV && !decryptiv) { 4155 logger.error(`Invalid IV: ${keyAttrs.IV}`); 4156 } 4157 // If decrypturi is a URI with a scheme, then baseurl will be ignored 4158 // No uri is allowed when METHOD is NONE 4159 const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : ''; 4160 const keyFormatVersions = (decryptkeyformatversions ? decryptkeyformatversions : '1').split('/').map(Number).filter(Number.isFinite); 4161 return new LevelKey(decryptmethod, resolvedUri, decryptkeyformat, keyFormatVersions, decryptiv); 4162} 4163function parseStartTimeOffset(startAttributes) { 4164 const startAttrs = new AttrList(startAttributes); 4165 const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET'); 4166 if (isFiniteNumber(startTimeOffset)) { 4167 return startTimeOffset; 4168 } 4169 return null; 4170} 4171function setCodecs(codecsAttributeValue, level) { 4172 let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter(c => c); 4173 ['video', 'audio', 'text'].forEach(type => { 4174 const filtered = codecs.filter(codec => isCodecType(codec, type)); 4175 if (filtered.length) { 4176 // Comma separated list of all codecs for type 4177 level[`${type}Codec`] = filtered.join(','); 4178 // Remove known codecs so that only unknownCodecs are left after iterating through each type 4179 codecs = codecs.filter(codec => filtered.indexOf(codec) === -1); 4180 } 4181 }); 4182 level.unknownCodecs = codecs; 4183} 4184function assignCodec(media, groupItem, codecProperty) { 4185 const codecValue = groupItem[codecProperty]; 4186 if (codecValue) { 4187 media[codecProperty] = codecValue; 4188 } 4189} 4190function backfillProgramDateTimes(fragments, firstPdtIndex) { 4191 let fragPrev = fragments[firstPdtIndex]; 4192 for (let i = firstPdtIndex; i--;) { 4193 const frag = fragments[i]; 4194 // Exit on delta-playlist skipped segments 4195 if (!frag) { 4196 return; 4197 } 4198 frag.programDateTime = fragPrev.programDateTime - frag.duration * 1000; 4199 fragPrev = frag; 4200 } 4201} 4202function assignProgramDateTime(frag, prevFrag) { 4203 if (frag.rawProgramDateTime) { 4204 frag.programDateTime = Date.parse(frag.rawProgramDateTime); 4205 } else if (prevFrag != null && prevFrag.programDateTime) { 4206 frag.programDateTime = prevFrag.endProgramDateTime; 4207 } 4208 if (!isFiniteNumber(frag.programDateTime)) { 4209 frag.programDateTime = null; 4210 frag.rawProgramDateTime = null; 4211 } 4212} 4213function setInitSegment(frag, mapAttrs, id, levelkeys) { 4214 frag.relurl = mapAttrs.URI; 4215 if (mapAttrs.BYTERANGE) { 4216 frag.setByteRange(mapAttrs.BYTERANGE); 4217 } 4218 frag.level = id; 4219 frag.sn = 'initSegment'; 4220 if (levelkeys) { 4221 frag.levelkeys = levelkeys; 4222 } 4223 frag.initSegment = null; 4224} 4225function setFragLevelKeys(frag, levelkeys, level) { 4226 frag.levelkeys = levelkeys; 4227 const { 4228 encryptedFragments 4229 } = level; 4230 if ((!encryptedFragments.length || encryptedFragments[encryptedFragments.length - 1].levelkeys !== levelkeys) && Object.keys(levelkeys).some(format => levelkeys[format].isCommonEncryption)) { 4231 encryptedFragments.push(frag); 4232 } 4233} 4234 4235var PlaylistContextType = { 4236 MANIFEST: "manifest", 4237 LEVEL: "level", 4238 AUDIO_TRACK: "audioTrack", 4239 SUBTITLE_TRACK: "subtitleTrack" 4240}; 4241var PlaylistLevelType = { 4242 MAIN: "main", 4243 AUDIO: "audio", 4244 SUBTITLE: "subtitle" 4245}; 4246 4247function mapContextToLevelType(context) { 4248 const { 4249 type 4250 } = context; 4251 switch (type) { 4252 case PlaylistContextType.AUDIO_TRACK: 4253 return PlaylistLevelType.AUDIO; 4254 case PlaylistContextType.SUBTITLE_TRACK: 4255 return PlaylistLevelType.SUBTITLE; 4256 default: 4257 return PlaylistLevelType.MAIN; 4258 } 4259} 4260function getResponseUrl(response, context) { 4261 let url = response.url; 4262 // responseURL not supported on some browsers (it is used to detect URL redirection) 4263 // data-uri mode also not supported (but no need to detect redirection) 4264 if (url === undefined || url.indexOf('data:') === 0) { 4265 // fallback to initial URL 4266 url = context.url; 4267 } 4268 return url; 4269} 4270class PlaylistLoader { 4271 constructor(hls) { 4272 this.hls = void 0; 4273 this.loaders = Object.create(null); 4274 this.variableList = null; 4275 this.hls = hls; 4276 this.registerListeners(); 4277 } 4278 startLoad(startPosition) {} 4279 stopLoad() { 4280 this.destroyInternalLoaders(); 4281 } 4282 registerListeners() { 4283 const { 4284 hls 4285 } = this; 4286 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4287 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 4288 hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); 4289 hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); 4290 } 4291 unregisterListeners() { 4292 const { 4293 hls 4294 } = this; 4295 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4296 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 4297 hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); 4298 hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); 4299 } 4300 4301 /** 4302 * Returns defaults or configured loader-type overloads (pLoader and loader config params) 4303 */ 4304 createInternalLoader(context) { 4305 const config = this.hls.config; 4306 const PLoader = config.pLoader; 4307 const Loader = config.loader; 4308 const InternalLoader = PLoader || Loader; 4309 const loader = new InternalLoader(config); 4310 this.loaders[context.type] = loader; 4311 return loader; 4312 } 4313 getInternalLoader(context) { 4314 return this.loaders[context.type]; 4315 } 4316 resetInternalLoader(contextType) { 4317 if (this.loaders[contextType]) { 4318 delete this.loaders[contextType]; 4319 } 4320 } 4321 4322 /** 4323 * Call `destroy` on all internal loader instances mapped (one per context type) 4324 */ 4325 destroyInternalLoaders() { 4326 for (const contextType in this.loaders) { 4327 const loader = this.loaders[contextType]; 4328 if (loader) { 4329 loader.destroy(); 4330 } 4331 this.resetInternalLoader(contextType); 4332 } 4333 } 4334 destroy() { 4335 this.variableList = null; 4336 this.unregisterListeners(); 4337 this.destroyInternalLoaders(); 4338 } 4339 onManifestLoading(event, data) { 4340 const { 4341 url 4342 } = data; 4343 this.variableList = null; 4344 this.load({ 4345 id: null, 4346 level: 0, 4347 responseType: 'text', 4348 type: PlaylistContextType.MANIFEST, 4349 url, 4350 deliveryDirectives: null 4351 }); 4352 } 4353 onLevelLoading(event, data) { 4354 const { 4355 id, 4356 level, 4357 pathwayId, 4358 url, 4359 deliveryDirectives 4360 } = data; 4361 this.load({ 4362 id, 4363 level, 4364 pathwayId, 4365 responseType: 'text', 4366 type: PlaylistContextType.LEVEL, 4367 url, 4368 deliveryDirectives 4369 }); 4370 } 4371 onAudioTrackLoading(event, data) { 4372 const { 4373 id, 4374 groupId, 4375 url, 4376 deliveryDirectives 4377 } = data; 4378 this.load({ 4379 id, 4380 groupId, 4381 level: null, 4382 responseType: 'text', 4383 type: PlaylistContextType.AUDIO_TRACK, 4384 url, 4385 deliveryDirectives 4386 }); 4387 } 4388 onSubtitleTrackLoading(event, data) { 4389 const { 4390 id, 4391 groupId, 4392 url, 4393 deliveryDirectives 4394 } = data; 4395 this.load({ 4396 id, 4397 groupId, 4398 level: null, 4399 responseType: 'text', 4400 type: PlaylistContextType.SUBTITLE_TRACK, 4401 url, 4402 deliveryDirectives 4403 }); 4404 } 4405 load(context) { 4406 var _context$deliveryDire; 4407 const config = this.hls.config; 4408 4409 // logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}
4409, id: ${context.id}`); 4410 4411 // Check if a loader for this context already exists 4412 let loader = this.getInternalLoader(context); 4413 if (loader) { 4414 const loaderContext = loader.context; 4415 if (loaderContext && loaderContext.url === context.url && loaderContext.level === context.level) { 4416 // same URL can't overlap 4417 logger.trace('[playlist-loader]: playlist request ongoing'); 4418 return; 4419 } 4420 logger.log(`[playlist-loader]: aborting previous loader for type: ${context.type}`); 4421 loader.abort(); 4422 } 4423 4424 // apply different configs for retries depending on 4425 // context (manifest, level, audio/subs playlist) 4426 let loadPolicy; 4427 if (context.type === PlaylistContextType.MANIFEST) { 4428 loadPolicy = config.manifestLoadPolicy.default; 4429 } else { 4430 loadPolicy = _extends({}, config.playlistLoadPolicy.default, { 4431 timeoutRetry: null, 4432 errorRetry: null 4433 }); 4434 } 4435 loader = this.createInternalLoader(context); 4436 4437 // Override level/track timeout for LL-HLS requests 4438 // (the default of 10000ms is counter productive to blocking playlist reload requests) 4439 if (isFiniteNumber((_context$deliveryDire = context.deliveryDirectives) == null ? void 0 : _context$deliveryDire.part)) { 4440 let levelDetails; 4441 if (context.type === PlaylistContextType.LEVEL && context.level !== null) { 4442 levelDetails = this.hls.levels[context.level].details; 4443 } else if (context.type === PlaylistContextType.AUDIO_TRACK && context.id !== null) { 4444 levelDetails = this.hls.audioTracks[context.id].details; 4445 } else if (context.type === PlaylistContextType.SUBTITLE_TRACK && context.id !== null) { 4446 levelDetails = this.hls.subtitleTracks[context.id].details; 4447 } 4448 if (levelDetails) { 4449 const partTarget = levelDetails.partTarget; 4450 const targetDuration = levelDetails.targetduration; 4451 if (partTarget && targetDuration) { 4452 const maxLowLatencyPlaylistRefresh = Math.max(partTarget * 3, targetDuration * 0.8) * 1000; 4453 loadPolicy = _extends({}, loadPolicy, { 4454 maxTimeToFirstByteMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs), 4455 maxLoadTimeMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs) 4456 }); 4457 } 4458 } 4459 } 4460 const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; 4461 const loaderConfig = { 4462 loadPolicy, 4463 timeout: loadPolicy.maxLoadTimeMs, 4464 maxRetry: legacyRetryCompatibility.maxNumRetry || 0, 4465 retryDelay: legacyRetryCompatibility.retryDelayMs || 0, 4466 maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 4467 }; 4468 const loaderCallbacks = { 4469 onSuccess: (response, stats, context, networkDetails) => { 4470 const loader = this.getInternalLoader(context); 4471 this.resetInternalLoader(context.type); 4472 const string = response.data; 4473 4474 // Validate if it is an M3U8 at all 4475 if (string.indexOf('#EXTM3U') !== 0) { 4476 this.handleManifestParsingError(response, context, new Error('no EXTM3U delimiter'), networkDetails || null, stats); 4477 return; 4478 } 4479 stats.parsing.start = performance.now(); 4480 if (M3U8Parser.isMediaPlaylist(string)) { 4481 this.handleTrackOrLevelPlaylist(response, stats, context, networkDetails || null, loader); 4482 } else { 4483 this.handleMasterPlaylist(response, stats, context, networkDetails); 4484 } 4485 }, 4486 onError: (response, context, networkDetails, stats) => { 4487 this.handleNetworkError(context, networkDetails, false, response, stats); 4488 }, 4489 onTimeout: (stats, context, networkDetails) => { 4490 this.handleNetworkError(context, networkDetails, true, undefined, stats); 4491 } 4492 }; 4493 4494 // logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`); 4495 4496 loader.load(context, loaderConfig, loaderCallbacks); 4497 } 4498 handleMasterPlaylist(response, stats, context, networkDetails) { 4499 const hls = this.hls; 4500 const string = response.data; 4501 const url = getResponseUrl(response, context); 4502 const parsedResult = M3U8Parser.parseMasterPlaylist(string, url); 4503 if (parsedResult.playlistParsingError) { 4504 this.handleManifestParsingError(response, context, parsedResult.playlistParsingError, networkDetails, stats); 4505 return; 4506 } 4507 const { 4508 contentSteering, 4509 levels, 4510 sessionData, 4511 sessionKeys, 4512 startTimeOffset, 4513 variableList 4514 } = parsedResult; 4515 this.variableList = variableList; 4516 const { 4517 AUDIO: audioTracks = [], 4518 SUBTITLES: subtitles, 4519 'CLOSED-CAPTIONS': captions 4520 } = M3U8Parser.parseMasterPlaylistMedia(string, url, parsedResult); 4521 if (audioTracks.length) { 4522 // check if we have found an audio track embedded in main playlist (audio track without URI attribute) 4523 const embeddedAudioFound = audioTracks.some(audioTrack => !audioTrack.url); 4524 4525 // if no embedded audio track defined, but audio codec signaled in quality level, 4526 // we need to signal this main audio track this could happen with playlists with 4527 // alt audio rendition in which quality levels (main) 4528 // contains both audio+video. but with mixed audio track not signaled 4529 if (!embeddedAudioFound && levels[0].audioCodec && !levels[0].attrs.AUDIO) { 4530 logger.log('[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one'); 4531 audioTracks.unshift({ 4532 type: 'main', 4533 name: 'main', 4534 groupId: 'main', 4535 default: false, 4536 autoselect: false, 4537 forced: false, 4538 id: -1, 4539 attrs: new AttrList({}), 4540 bitrate: 0, 4541 url: '' 4542 }); 4543 } 4544 } 4545 hls.trigger(Events.MANIFEST_LOADED, { 4546 levels, 4547 audioTracks, 4548 subtitles, 4549 captions, 4550 contentSteering, 4551 url, 4552 stats, 4553 networkDetails, 4554 sessionData, 4555 sessionKeys, 4556 startTimeOffset, 4557 variableList 4558 }); 4559 } 4560 handleTrackOrLevelPlaylist(response, stats, context, networkDetails, loader) { 4561 const hls = this.hls; 4562 const { 4563 id, 4564 level, 4565 type 4566 } = context; 4567 const url = getResponseUrl(response, context); 4568 const levelUrlId = 0; 4569 const levelId = isFiniteNumber(level) ? level : isFiniteNumber(id) ? id : 0; 4570 const levelType = mapContextToLevelType(context); 4571 const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, levelUrlId, this.variableList); 4572 4573 // We have done our first request (Manifest-type) and receive 4574 // not a master playlist but a chunk-list (track/level) 4575 // We fire the manifest-loaded event anyway with the parsed level-details 4576 // by creating a single-level structure for it. 4577 if (type === PlaylistContextType.MANIFEST) { 4578 const singleLevel = { 4579 attrs: new AttrList({}), 4580 bitrate: 0, 4581 details: levelDetails, 4582 name: '', 4583 url 4584 }; 4585 hls.trigger(Events.MANIFEST_LOADED, { 4586 levels: [singleLevel], 4587 audioTracks: [], 4588 url, 4589 stats, 4590 networkDetails, 4591 sessionData: null, 4592 sessionKeys: null, 4593 contentSteering: null, 4594 startTimeOffset: null, 4595 variableList: null 4596 }); 4597 } 4598 4599 // save parsing time 4600 stats.parsing.end = performance.now(); 4601 4602 // extend the context with the new levelDetails property 4603 context.levelDetails = levelDetails; 4604 this.handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader); 4605 } 4606 handleManifestParsingError(response, context, error, networkDetails, stats) { 4607 this.hls.trigger(Events.ERROR, {
4608 type: ErrorTypes.NETWORK_ERROR, 4609 details: ErrorDetails.MANIFEST_PARSING_ERROR, 4610 fatal: context.type === PlaylistContextType.MANIFEST, 4611 url: response.url, 4612 err: error, 4613 error, 4614 reason: error.message, 4615 response, 4616 context, 4617 networkDetails, 4618 stats 4619 }); 4620 } 4621 handleNetworkError(context, networkDetails, timeout = false, response, stats) { 4622 let message = `A network ${timeout ? 'timeout' : 'error' + (response ? ' (status ' + response.code + ')' : '')} occurred while loading ${context.type}`; 4623 if (context.type === PlaylistContextType.LEVEL) { 4624 message += `: ${context.level} id: ${context.id}`; 4625 } else if (context.type === PlaylistContextType.AUDIO_TRACK || context.type === PlaylistContextType.SUBTITLE_TRACK) { 4626 message += ` id: ${context.id} group-id: "${context.groupId}"`; 4627 } 4628 const error = new Error(message); 4629 logger.warn(`[playlist-loader]: ${message}`); 4630 let details = ErrorDetails.UNKNOWN; 4631 let fatal = false; 4632 const loader = this.getInternalLoader(context); 4633 switch (context.type) { 4634 case PlaylistContextType.MANIFEST: 4635 details = timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR; 4636 fatal = true; 4637 break; 4638 case PlaylistContextType.LEVEL: 4639 details = timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR; 4640 fatal = false; 4641 break; 4642 case PlaylistContextType.AUDIO_TRACK: 4643 details = timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR; 4644 fatal = false; 4645 break; 4646 case PlaylistContextType.SUBTITLE_TRACK: 4647 details = timeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_LOAD_ERROR; 4648 fatal = false; 4649 break; 4650 } 4651 if (loader) { 4652 this.resetInternalLoader(context.type); 4653 } 4654 const errorData = { 4655 type: ErrorTypes.NETWORK_ERROR, 4656 details, 4657 fatal, 4658 url: context.url, 4659 loader, 4660 context, 4661 error, 4662 networkDetails, 4663 stats 4664 }; 4665 if (response) { 4666 const url = (networkDetails == null ? void 0 : networkDetails.url) || context.url; 4667 errorData.response = _objectSpread2({ 4668 url, 4669 data: undefined 4670 }, response); 4671 } 4672 this.hls.trigger(Events.ERROR, errorData); 4673 } 4674 handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader) { 4675 const hls = this.hls; 4676 const { 4677 type, 4678 level, 4679 id, 4680 groupId, 4681 deliveryDirectives 4682 } = context; 4683 const url = getResponseUrl(response, context); 4684 const parent = mapContextToLevelType(context); 4685 const levelIndex = typeof context.level === 'number' && parent === PlaylistLevelType.MAIN ? level : undefined; 4686 if (!levelDetails.fragments.length) { 4687 const _error = new Error('No Segments found in Playlist'); 4688 hls.trigger(Events.ERROR, { 4689 type: ErrorTypes.NETWORK_ERROR, 4690 details: ErrorDetails.LEVEL_EMPTY_ERROR, 4691 fatal: false, 4692 url, 4693 error: _error, 4694 reason: _error.message, 4695 response, 4696 context, 4697 level: levelIndex, 4698 parent, 4699 networkDetails, 4700 stats 4701 }); 4702 return; 4703 } 4704 if (!levelDetails.targetduration) { 4705 levelDetails.playlistParsingError = new Error('Missing Target Duration'); 4706 } 4707 const error = levelDetails.playlistParsingError; 4708 if (error) { 4709 hls.trigger(Events.ERROR, { 4710 type: ErrorTypes.NETWORK_ERROR, 4711 details: ErrorDetails.LEVEL_PARSING_ERROR, 4712 fatal: false, 4713 url, 4714 error, 4715 reason: error.message, 4716 response, 4717 context, 4718 level: levelIndex, 4719 parent, 4720 networkDetails, 4721 stats 4722 }); 4723 return; 4724 } 4725 if (levelDetails.live && loader) { 4726 if (loader.getCacheAge) { 4727 levelDetails.ageHeader = loader.getCacheAge() || 0; 4728 } 4729 if (!loader.getCacheAge || isNaN(levelDetails.ageHeader)) { 4730 levelDetails.ageHeader = 0; 4731 } 4732 } 4733 switch (type) { 4734 case PlaylistContextType.MANIFEST: 4735 case PlaylistContextType.LEVEL: 4736 hls.trigger(Events.LEVEL_LOADED, { 4737 details: levelDetails, 4738 level: levelIndex || 0, 4739 id: id || 0, 4740 stats, 4741 networkDetails, 4742 deliveryDirectives 4743 }); 4744 break; 4745 case PlaylistContextType.AUDIO_TRACK: 4746 hls.trigger(Events.AUDIO_TRACK_LOADED, { 4747 details: levelDetails, 4748 id: id || 0, 4749 groupId: groupId || '', 4750 stats, 4751 networkDetails, 4752 deliveryDirectives 4753 }); 4754 break; 4755 case PlaylistContextType.SUBTITLE_TRACK: 4756 hls.trigger(Events.SUBTITLE_TRACK_LOADED, { 4757 details: levelDetails, 4758 id: id || 0, 4759 groupId: groupId || '', 4760 stats, 4761 networkDetails, 4762 deliveryDirectives 4763 }); 4764 break; 4765 } 4766 } 4767} 4768 4769function sendAddTrackEvent(track, videoEl) { 4770 let event; 4771 try { 4772 event = new Event('addtrack'); 4773 } catch (err) { 4774 // for IE11 4775 event = document.createEvent('Event'); 4776 event.initEvent('addtrack', false, false); 4777 } 4778 event.track = track; 4779 videoEl.dispatchEvent(event); 4780} 4781function addCueToTrack(track, cue) { 4782 // Sometimes there are cue overlaps on segmented vtts so the same 4783 // cue can appear more than once in different vtt files. 4784 // This avoid showing duplicated cues with same timecode and text. 4785 const mode = track.mode; 4786 if (mode === 'disabled') { 4787 track.mode = 'hidden'; 4788 } 4789 if (track.cues && !track.cues.getCueById(cue.id)) { 4790 try { 4791 track.addCue(cue); 4792 if (!track.cues.getCueById(cue.id)) { 4793 throw new Error(`addCue is failed for: ${cue}`); 4794 } 4795 } catch (err) { 4796 logger.debug(`[texttrack-utils]: ${err}`);
4797 try { 4798 const textTrackCue = new self.TextTrackCue(cue.startTime, cue.endTime, cue.text); 4799 textTrackCue.id = cue.id; 4800 track.addCue(textTrackCue); 4801 } catch (err2) { 4802 logger.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${err2}`); 4803 } 4804 } 4805 } 4806 if (mode === 'disabled') { 4807 track.mode = mode; 4808 } 4809} 4810function clearCurrentCues(track) { 4811 // When track.mode is disabled, track.cues will be null. 4812 // To guarantee the removal of cues, we need to temporarily 4813 // change the mode to hidden 4814 const mode = track.mode; 4815 if (mode === 'disabled') { 4816 track.mode = 'hidden'; 4817 } 4818 if (track.cues) { 4819 for (let i = track.cues.length; i--;) { 4820 track.removeCue(track.cues[i]); 4821 } 4822 } 4823 if (mode === 'disabled') { 4824 track.mode = mode; 4825 } 4826} 4827function removeCuesInRange(track, start, end, predicate) { 4828 const mode = track.mode; 4829 if (mode === 'disabled') { 4830 track.mode = 'hidden'; 4831 } 4832 if (track.cues && track.cues.length > 0) { 4833 const cues = getCuesInRange(track.cues, start, end); 4834 for (let i = 0; i < cues.length; i++) { 4835 if (!predicate || predicate(cues[i])) { 4836 track.removeCue(cues[i]); 4837 } 4838 } 4839 } 4840 if (mode === 'disabled') { 4841 track.mode = mode; 4842 } 4843} 4844 4845// Find first cue starting after given time. 4846// Modified version of binary search O(log(n)). 4847function getFirstCueIndexAfterTime(cues, time) { 4848 // If first cue starts after time, start there 4849 if (time < cues[0].startTime) { 4850 return 0; 4851 } 4852 // If the last cue ends before time there is no overlap 4853 const len = cues.length - 1; 4854 if (time > cues[len].endTime) { 4855 return -1; 4856 } 4857 let left = 0; 4858 let right = len; 4859 while (left <= right) { 4860 const mid = Math.floor((right + left) / 2); 4861 if (time < cues[mid].startTime) { 4862 right = mid - 1; 4863 } else if (time > cues[mid].startTime && left < len) { 4864 left = mid + 1; 4865 } else { 4866 // If it's not lower or higher, it must be equal. 4867 return mid; 4868 } 4869 } 4870 // At this point, left and right have swapped. 4871 // No direct match was found, left or right element must be the closest. Check which one has the smallest diff. 4872 return cues[left].startTime - time < time - cues[right].startTime ? left : right; 4873} 4874function getCuesInRange(cues, start, end) { 4875 const cuesFound = []; 4876 const firstCueInRange = getFirstCueIndexAfterTime(cues, start); 4877 if (firstCueInRange > -1) { 4878 for (let i = firstCueInRange, len = cues.length; i < len; i++) { 4879 const cue = cues[i]; 4880 if (cue.startTime >= start && cue.endTime <= end) { 4881 cuesFound.push(cue); 4882 } else if (cue.startTime > end) { 4883 return cuesFound; 4884 } 4885 } 4886 } 4887 return cuesFound; 4888} 4889function filterSubtitleTracks(textTrackList) { 4890 const tracks = []; 4891 for (let i = 0; i < textTrackList.length; i++) { 4892 const track = textTrackList[i]; 4893 // Edge adds a track without a label; we don't want to use it 4894 if ((track.kind === 'subtitles' || track.kind === 'captions') && track.label) { 4895 tracks.push(textTrackList[i]); 4896 } 4897 } 4898 return tracks; 4899} 4900 4901var MetadataSchema = { 4902 audioId3: "org.id3", 4903 dateRange: "com.apple.quicktime.HLS", 4904 emsg: "https://aomedia.org/emsg/ID3" 4905}; 4906 4907const MIN_CUE_DURATION = 0.25; 4908function getCueClass() { 4909 if (typeof self === 'undefined') return undefined; 4910 return self.VTTCue || self.TextTrackCue; 4911} 4912function createCueWithDataFields(Cue, startTime, endTime, data, type) { 4913 let cue = new Cue(startTime, endTime, ''); 4914 try { 4915 cue.value = data; 4916 if (type) { 4917 cue.type = type; 4918 } 4919 } catch (e) { 4920 cue = new Cue(startTime, endTime, JSON.stringify(type ? _objectSpread2({ 4921 type 4922 }, data) : data)); 4923 } 4924 return cue; 4925} 4926 4927// VTTCue latest draft allows an infinite duration, fallback 4928// to MAX_VALUE if necessary 4929const MAX_CUE_ENDTIME = (() => { 4930 const Cue = getCueClass(); 4931 try { 4932 Cue && new Cue(0, Number.POSITIVE_INFINITY, ''); 4933 } catch (e) { 4934 return Number.MAX_VALUE; 4935 } 4936 return Number.POSITIVE_INFINITY; 4937})(); 4938function dateRangeDateToTimelineSeconds(date, offset) { 4939 return date.getTime() / 1000 - offset; 4940} 4941function hexToArrayBuffer(str) { 4942 return Uint8Array.from(str.replace(/^0x/, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')).buffer; 4943} 4944class ID3TrackController { 4945 constructor(hls) { 4946 this.hls = void 0; 4947 this.id3Track = null; 4948 this.media = null; 4949 this.dateRangeCuesAppended = {}; 4950 this.hls = hls; 4951 this._registerListeners(); 4952 } 4953 destroy() { 4954 this._unregisterListeners(); 4955 this.id3Track = null; 4956 this.media = null; 4957 this.dateRangeCuesAppended = {}; 4958 // @ts-ignore 4959 this.hls = null; 4960 } 4961 _registerListeners() { 4962 const { 4963 hls 4964 } = this;
4965 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 4966 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 4967 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4968 hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); 4969 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 4970 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 4971 } 4972 _unregisterListeners() { 4973 const { 4974 hls 4975 } = this; 4976 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 4977 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 4978 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4979 hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); 4980 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 4981 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 4982 } 4983 4984 // Add ID3 metatadata text track. 4985 onMediaAttached(event, data) { 4986 this.media = data.media; 4987 } 4988 onMediaDetaching() { 4989 if (!this.id3Track) { 4990 return; 4991 } 4992 clearCurrentCues(this.id3Track); 4993 this.id3Track = null; 4994 this.media = null; 4995 this.dateRangeCuesAppended = {}; 4996 } 4997 onManifestLoading() { 4998 this.dateRangeCuesAppended = {}; 4999 } 5000 createTrack(media) { 5001 const track = this.getID3Track(media.textTracks); 5002 track.mode = 'hidden'; 5003 return track; 5004 } 5005 getID3Track(textTracks) { 5006 if (!this.media) { 5007 return; 5008 } 5009 for (let i = 0; i < textTracks.length; i++) { 5010 const textTrack = textTracks[i]; 5011 if (textTrack.kind === 'metadata' && textTrack.label === 'id3') { 5012 // send 'addtrack' when reusing the textTrack for metadata, 5013 // same as what we do for captions 5014 sendAddTrackEvent(textTrack, this.media); 5015 return textTrack; 5016 } 5017 } 5018 return this.media.addTextTrack('metadata', 'id3'); 5019 } 5020 onFragParsingMetadata(event, data) { 5021 if (!this.media) { 5022 return; 5023 } 5024 const { 5025 hls: { 5026 config: { 5027 enableEmsgMetadataCues, 5028 enableID3MetadataCues 5029 } 5030 } 5031 } = this; 5032 if (!enableEmsgMetadataCues && !enableID3MetadataCues) { 5033 return; 5034 } 5035 const { 5036 samples 5037 } = data; 5038 5039 // create track dynamically 5040 if (!this.id3Track) { 5041 this.id3Track = this.createTrack(this.media); 5042 } 5043 const Cue = getCueClass(); 5044 if (!Cue) { 5045 return; 5046 } 5047 for (let i = 0; i < samples.length; i++) { 5048 const type = samples[i].type; 5049 if (type === MetadataSchema.emsg && !enableEmsgMetadataCues || !enableID3MetadataCues) { 5050 continue; 5051 } 5052 const frames = getID3Frames(samples[i].data); 5053 if (frames) { 5054 const startTime = samples[i].pts; 5055 let endTime = startTime + samples[i].duration; 5056 if (endTime > MAX_CUE_ENDTIME) { 5057 endTime = MAX_CUE_ENDTIME; 5058 } 5059 const timeDiff = endTime - startTime; 5060 if (timeDiff <= 0) { 5061 endTime = startTime + MIN_CUE_DURATION; 5062 } 5063 for (let j = 0; j < frames.length; j++) { 5064 const frame = frames[j]; 5065 // Safari doesn't put the timestamp frame in the TextTrack 5066 if (!isTimeStampFrame(frame)) { 5067 // add a bounds to any unbounded cues 5068 this.updateId3CueEnds(startTime, type); 5069 const cue = createCueWithDataFields(Cue, startTime, endTime, frame, type); 5070 if (cue) { 5071 this.id3Track.addCue(cue); 5072 } 5073 } 5074 } 5075 } 5076 } 5077 } 5078 updateId3CueEnds(startTime, type) { 5079 var _this$id3Track; 5080 const cues = (_this$id3Track = this.id3Track) == null ? void 0 : _this$id3Track.cues; 5081 if (cues) { 5082 for (let i = cues.length; i--;) { 5083 const cue = cues[i]; 5084 if (cue.type === type && cue.startTime < startTime && cue.endTime === MAX_CUE_ENDTIME) { 5085 cue.endTime = startTime; 5086 } 5087 } 5088 } 5089 } 5090 onBufferFlushing(event, { 5091 startOffset, 5092 endOffset, 5093 type 5094 }) { 5095 const { 5096 id3Track, 5097 hls 5098 } = this; 5099 if (!hls) { 5100 return; 5101 } 5102 const { 5103 config: { 5104 enableEmsgMetadataCues, 5105 enableID3MetadataCues 5106 } 5107 } = hls; 5108 if (id3Track && (enableEmsgMetadataCues || enableID3MetadataCues)) { 5109 let predicate; 5110 if (type === 'audio') { 5111 predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues; 5112 } else if (type === 'video') { 5113 predicate = cue => cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; 5114 } else { 5115 predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues || cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; 5116 } 5117 removeCuesInRange(id3Track, startOffset, endOffset, predicate); 5118 } 5119 } 5120 onLevelUpdated(event, { 5121 details 5122 }) { 5123 if (!this.media || !details.hasProgramDateTime || !this.hls.config.enableDateRangeMetadataCues) { 5124 return; 5125 } 5126 const { 5127 dateRangeCuesAppended, 5128 id3Track 5129 } = this; 5130 const { 5131 dateRanges 5132 } = details; 5133 const ids = Object.keys(dateRanges); 5134 // Remove cues from track not found in details.dateRanges 5135 if (id3Track) { 5136 const idsToRemove = Object.keys(dateRangeCuesAppended).filter(id => !ids.includes(id)); 5137 for (let i = idsToRemove.length; i--;) { 5138 const id = idsToRemove[i];
5139 Object.keys(dateRangeCuesAppended[id].cues).forEach(key => { 5140 id3Track.removeCue(dateRangeCuesAppended[id].cues[key]); 5141 }); 5142 delete dateRangeCuesAppended[id]; 5143 } 5144 } 5145 // Exit if the playlist does not have Date Ranges or does not have Program Date Time 5146 const lastFragment = details.fragments[details.fragments.length - 1]; 5147 if (ids.length === 0 || !isFiniteNumber(lastFragment == null ? void 0 : lastFragment.programDateTime)) { 5148 return; 5149 } 5150 if (!this.id3Track) { 5151 this.id3Track = this.createTrack(this.media); 5152 } 5153 const dateTimeOffset = lastFragment.programDateTime / 1000 - lastFragment.start; 5154 const Cue = getCueClass(); 5155 for (let i = 0; i < ids.length; i++) { 5156 const id = ids[i]; 5157 const dateRange = dateRanges[id]; 5158 const startTime = dateRangeDateToTimelineSeconds(dateRange.startDate, dateTimeOffset); 5159 5160 // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT) 5161 const appendedDateRangeCues = dateRangeCuesAppended[id]; 5162 const cues = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.cues) || {}; 5163 let durationKnown = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.durationKnown) || false; 5164 let endTime = MAX_CUE_ENDTIME; 5165 const endDate = dateRange.endDate; 5166 if (endDate) { 5167 endTime = dateRangeDateToTimelineSeconds(endDate, dateTimeOffset); 5168 durationKnown = true; 5169 } else if (dateRange.endOnNext && !durationKnown) { 5170 const nextDateRangeWithSameClass = ids.reduce((candidateDateRange, id) => { 5171 if (id !== dateRange.id) { 5172 const otherDateRange = dateRanges[id]; 5173 if (otherDateRange.class === dateRange.class && otherDateRange.startDate > dateRange.startDate && (!candidateDateRange || dateRange.startDate < candidateDateRange.startDate)) { 5174 return otherDateRange; 5175 } 5176 } 5177 return candidateDateRange; 5178 }, null); 5179 if (nextDateRangeWithSameClass) { 5180 endTime = dateRangeDateToTimelineSeconds(nextDateRangeWithSameClass.startDate, dateTimeOffset); 5181 durationKnown = true; 5182 } 5183 } 5184 5185 // Create TextTrack Cues for each MetadataGroup Item (select DateRange attribute) 5186 // This is to emulate Safari HLS playback handling of DateRange tags 5187 const attributes = Object.keys(dateRange.attr); 5188 for (let j = 0; j < attributes.length; j++) { 5189 const key = attributes[j]; 5190 if (!isDateRangeCueAttribute(key)) { 5191 continue; 5192 } 5193 const cue = cues[key]; 5194 if (cue) { 5195 if (durationKnown && !appendedDateRangeCues.durationKnown) { 5196 cue.endTime = endTime; 5197 } 5198 } else if (Cue) { 5199 let data = dateRange.attr[key]; 5200 if (isSCTE35Attribute(key)) { 5201 data = hexToArrayBuffer(data); 5202 } 5203 const _cue = createCueWithDataFields(Cue, startTime, endTime, { 5204 key, 5205 data 5206 }, MetadataSchema.dateRange); 5207 if (_cue) { 5208 _cue.id = id; 5209 this.id3Track.addCue(_cue); 5210 cues[key] = _cue; 5211 } 5212 } 5213 } 5214 5215 // Keep track of processed DateRanges by ID for updating cues with new DateRange tag attributes 5216 dateRangeCuesAppended[id] = { 5217 cues, 5218 dateRange, 5219 durationKnown 5220 }; 5221 } 5222 } 5223} 5224 5225class LatencyController { 5226 constructor(hls) { 5227 this.hls = void 0; 5228 this.config = void 0; 5229 this.media = null; 5230 this.levelDetails = null; 5231 this.currentTime = 0; 5232 this.stallCount = 0; 5233 this._latency = null; 5234 this.timeupdateHandler = () => this.timeupdate(); 5235 this.hls = hls; 5236 this.config = hls.config; 5237 this.registerListeners(); 5238 } 5239 get latency() { 5240 return this._latency || 0; 5241 } 5242 get maxLatency() { 5243 const { 5244 config, 5245 levelDetails 5246 } = this; 5247 if (config.liveMaxLatencyDuration !== undefined) { 5248 return config.liveMaxLatencyDuration; 5249 } 5250 return levelDetails ? config.liveMaxLatencyDurationCount * levelDetails.targetduration : 0; 5251 } 5252 get targetLatency() { 5253 const { 5254 levelDetails 5255 } = this; 5256 if (levelDetails === null) { 5257 return null; 5258 } 5259 const { 5260 holdBack, 5261 partHoldBack, 5262 targetduration 5263 } = levelDetails; 5264 const { 5265 liveSyncDuration, 5266 liveSyncDurationCount, 5267 lowLatencyMode 5268 } = this.config; 5269 const userConfig = this.hls.userConfig; 5270 let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack; 5271 if (userConfig.liveSyncDuration || userConfig.liveSyncDurationCount || targetLatency === 0) { 5272 targetLatency = liveSyncDuration !== undefined ? liveSyncDuration : liveSyncDurationCount * targetduration; 5273 } 5274 const maxLiveSyncOnStallIncrease = targetduration; 5275 const liveSyncOnStallIncrease = 1.0; 5276 return targetLatency + Math.min(this.stallCount * liveSyncOnStallIncrease, maxLiveSyncOnStallIncrease); 5277 } 5278 get liveSyncPosition() { 5279 const liveEdge = this.estimateLiveEdge(); 5280 const targetLatency = this.targetLatency; 5281 const levelDetails = this.levelDetails; 5282 if (liveEdge === null || targetLatency === null || levelDetails === null) { 5283 return null; 5284 } 5285 const edge = levelDetails.edge; 5286 const syncPosition = liveEdge - targetLatency - this.edgeStalled; 5287 const min = edge - levelDetails.totalduration; 5288 const max = edge - (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration); 5289 return Math.min(Math.max(min, syncPosition), max); 5290 } 5291 get drift() { 5292 const { 5293 levelDetails 5294 } = this; 5295 if (levelDetails === null) { 5296 return 1; 5297 } 5298 return levelDetails.drift; 5299 } 5300 get edgeStalled() { 5301 const { 5302 levelDetails 5303 } = this; 5304 if (levelDetails === null) { 5305 return 0; 5306 } 5307 const maxLevelUpdateAge = (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration) * 3; 5308 return Math.max(levelDetails.age - maxLevelUpdateAge, 0); 5309 } 5310 get forwardBufferLength() { 5311 const { 5312 media, 5313 levelDetails 5314 } = this; 5315 if (!media || !levelDetails) { 5316 return 0; 5317 } 5318 const bufferedRanges = media.buffered.length; 5319 return (bufferedRanges ? media.buffered.end(bufferedRanges - 1) : levelDetails.edge) - this.currentTime; 5320 } 5321 destroy() { 5322 this.unregisterListeners(); 5323 this.onMediaDetaching(); 5324 this.levelDetails = null; 5325 // @ts-ignore 5326 this.hls = this.timeupdateHandler = null; 5327 } 5328 registerListeners() {
vendor: 8,580 bytes, lines 5329-5616
5329 this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 5330 this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 5331 this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 5332 this.hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 5333 this.hls.on(Events.ERROR, this.onError, this); 5334 } 5335 unregisterListeners() { 5336 this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 5337 this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 5338 this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 5339 this.hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 5340 this.hls.off(Events.ERROR, this.onError, this); 5341 } 5342 onMediaAttached(event, data) { 5343 this.media = data.media; 5344 this.media.addEventListener('timeupdate', this.timeupdateHandler); 5345 } 5346 onMediaDetaching() { 5347 if (this.media) { 5348 this.media.removeEventListener('timeupdate', this.timeupdateHandler); 5349 this.media = null; 5350 } 5351 } 5352 onManifestLoading() { 5353 this.levelDetails = null; 5354 this._latency = null; 5355 this.stallCount = 0; 5356 } 5357 onLevelUpdated(event, { 5358 details 5359 }) { 5360 this.levelDetails = details; 5361 if (details.advanced) { 5362 this.timeupdate(); 5363 } 5364 if (!details.live && this.media) { 5365 this.media.removeEventListener('timeupdate', this.timeupdateHandler); 5366 } 5367 } 5368 onError(event, data) { 5369 var _this$levelDetails; 5370 if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) { 5371 return; 5372 } 5373 this.stallCount++; 5374 if ((_this$levelDetails = this.levelDetails) != null && _this$levelDetails.live) { 5375 logger.warn('[playback-rate-controller]: Stall detected, adjusting target latency'); 5376 } 5377 } 5378 timeupdate() { 5379 const { 5380 media, 5381 levelDetails 5382 } = this; 5383 if (!media || !levelDetails) { 5384 return; 5385 } 5386 this.currentTime = media.currentTime; 5387 const latency = this.computeLatency(); 5388 if (latency === null) { 5389 return; 5390 } 5391 this._latency = latency; 5392 5393 // Adapt playbackRate to meet target latency in low-latency mode 5394 const { 5395 lowLatencyMode, 5396 maxLiveSyncPlaybackRate 5397 } = this.config; 5398 if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1 || !levelDetails.live) { 5399 return; 5400 } 5401 const targetLatency = this.targetLatency; 5402 if (targetLatency === null) { 5403 return; 5404 } 5405 const distanceFromTarget = latency - targetLatency; 5406 // Only adjust playbackRate when within one target duration of targetLatency 5407 // and more than one second from under-buffering. 5408 // Playback further than one target duration from target can be considered DVR playback. 5409 const liveMinLatencyDuration = Math.min(this.maxLatency, targetLatency + levelDetails.targetduration); 5410 const inLiveRange = distanceFromTarget < liveMinLatencyDuration; 5411 if (inLiveRange && distanceFromTarget > 0.05 && this.forwardBufferLength > 1) { 5412 const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate)); 5413 const rate = Math.round(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled)) * 20) / 20; 5414 media.playbackRate = Math.min(max, Math.max(1, rate)); 5415 } else if (media.playbackRate !== 1 && media.playbackRate !== 0) { 5416 media.playbackRate = 1; 5417 } 5418 } 5419 estimateLiveEdge() { 5420 const { 5421 levelDetails 5422 } = this; 5423 if (levelDetails === null) { 5424 return null; 5425 } 5426 return levelDetails.edge + levelDetails.age; 5427 } 5428 computeLatency() { 5429 const liveEdge = this.estimateLiveEdge(); 5430 if (liveEdge === null) { 5431 return null; 5432 } 5433 return liveEdge - this.currentTime; 5434 } 5435} 5436 5437const HdcpLevels = ['NONE', 'TYPE-0', 'TYPE-1', null]; 5438function isHdcpLevel(value) { 5439 return HdcpLevels.indexOf(value) > -1; 5440} 5441const VideoRangeValues = ['SDR', 'PQ', 'HLG']; 5442function isVideoRange(value) { 5443 return !!value && VideoRangeValues.indexOf(value) > -1; 5444} 5445var HlsSkip = { 5446 No: "", 5447 Yes: "YES", 5448 v2: "v2" 5449}; 5450function getSkipValue(details) { 5451 const { 5452 canSkipUntil, 5453 canSkipDateRanges, 5454 age 5455 } = details; 5456 // A Client SHOULD NOT request a Playlist Delta Update unless it already 5457 // has a version of the Playlist that is no older than one-half of the Skip Boundary. 5458 // @see: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.3.7 5459 const playlistRecentEnough = age < canSkipUntil / 2; 5460 if (canSkipUntil && playlistRecentEnough) { 5461 if (canSkipDateRanges) { 5462 return HlsSkip.v2; 5463 } 5464 return HlsSkip.Yes; 5465 } 5466 return HlsSkip.No; 5467} 5468class HlsUrlParameters { 5469 constructor(msn, part, skip) { 5470 this.msn = void 0; 5471 this.part = void 0; 5472 this.skip = void 0; 5473 this.msn = msn; 5474 this.part = part; 5475 this.skip = skip; 5476 } 5477 addDirectives(uri) { 5478 const url = new self.URL(uri); 5479 if (this.msn !== undefined) { 5480 url.searchParams.set('_HLS_msn', this.msn.toString()); 5481 } 5482 if (this.part !== undefined) { 5483 url.searchParams.set('_HLS_part', this.part.toString()); 5484 } 5485 if (this.skip) { 5486 url.searchParams.set('_HLS_skip', this.skip); 5487 } 5488 return url.href; 5489 } 5490} 5491class Level { 5492 constructor(data) { 5493 this._attrs = void 0; 5494 this.audioCodec = void 0; 5495 this.bitrate = void 0; 5496 this.codecSet = void 0; 5497 this.url = void 0; 5498 this.frameRate = void 0; 5499 this.height = void 0; 5500 this.id = void 0; 5501 this.name = void 0; 5502 this.videoCodec = void 0; 5503 this.width = void 0; 5504 this.details = void 0; 5505 this.fragmentError = 0; 5506 this.loadError = 0; 5507 this.loaded = void 0; 5508 this.realBitrate = 0; 5509 this.supportedPromise = void 0; 5510 this.supportedResult = void 0; 5511 this._avgBitrate = 0; 5512 this._audioGroups = void 0; 5513 this._subtitleGroups = void 0; 5514 // Deprecated (retained for backwards compatibility) 5515 this._urlId = 0; 5516 this.url = [data.url]; 5517 this._attrs = [data.attrs]; 5518 this.bitrate = data.bitrate; 5519 if (data.details) { 5520 this.details = data.details; 5521 } 5522 this.id = data.id || 0; 5523 this.name = data.name; 5524 this.width = data.width || 0; 5525 this.height = data.height || 0; 5526 this.frameRate = data.attrs.optionalFloat('FRAME-RATE', 0); 5527 this._avgBitrate = data.attrs.decimalInteger('AVERAGE-BANDWIDTH'); 5528 this.audioCodec = data.audioCodec; 5529 this.videoCodec = data.videoCodec; 5530 this.codecSet = [data.videoCodec, data.audioCodec].filter(c => !!c).map(s => s.substring(0, 4)).join(','); 5531 this.addGroupId('audio', data.attrs.AUDIO); 5532 this.addGroupId('text', data.attrs.SUBTITLES); 5533 } 5534 get maxBitrate() { 5535 return Math.max(this.realBitrate, this.bitrate); 5536 } 5537 get averageBitrate() { 5538 return this._avgBitrate || this.realBitrate || this.bitrate; 5539 } 5540 get attrs() { 5541 return this._attrs[0]; 5542 } 5543 get codecs() { 5544 return this.attrs.CODECS || ''; 5545 } 5546 get pathwayId() { 5547 return this.attrs['PATHWAY-ID'] || '.'; 5548 } 5549 get videoRange() { 5550 return this.attrs['VIDEO-RANGE'] || 'SDR'; 5551 } 5552 get score() { 5553 return this.attrs.optionalFloat('SCORE', 0); 5554 } 5555 get uri() { 5556 return this.url[0] || ''; 5557 } 5558 hasAudioGroup(groupId) { 5559 return hasGroup(this._audioGroups, groupId); 5560 } 5561 hasSubtitleGroup(groupId) { 5562 return hasGroup(this._subtitleGroups, groupId); 5563 } 5564 get audioGroups() { 5565 return this._audioGroups; 5566 } 5567 get subtitleGroups() { 5568 return this._subtitleGroups; 5569 } 5570 addGroupId(type, groupId) { 5571 if (!groupId) { 5572 return; 5573 } 5574 if (type === 'audio') { 5575 let audioGroups = this._audioGroups; 5576 if (!audioGroups) { 5577 audioGroups = this._audioGroups = []; 5578 } 5579 if (audioGroups.indexOf(groupId) === -1) { 5580 audioGroups.push(groupId); 5581 } 5582 } else if (type === 'text') { 5583 let subtitleGroups = this._subtitleGroups; 5584 if (!subtitleGroups) { 5585 subtitleGroups = this._subtitleGroups = []; 5586 } 5587 if (subtitleGroups.indexOf(groupId) === -1) { 5588 subtitleGroups.push(groupId); 5589 } 5590 } 5591 } 5592 5593 // Deprecated methods (retained for backwards compatibility) 5594 get urlId() { 5595 return 0; 5596 } 5597 set urlId(value) {} 5598 get audioGroupIds() { 5599 return this.audioGroups ? [this.audioGroupId] : undefined; 5600 } 5601 get textGroupIds() { 5602 return this.subtitleGroups ? [this.textGroupId] : undefined; 5603 } 5604 get audioGroupId() { 5605 var _this$audioGroups; 5606 return (_this$audioGroups = this.audioGroups) == null ? void 0 : _this$audioGroups[0]; 5607 } 5608 get textGroupId() { 5609 var _this$subtitleGroups; 5610 return (_this$subtitleGroups = this.subtitleGroups) == null ? void 0 : _this$subtitleGroups[0]; 5611 } 5612 addFallback() {} 5613} 5614function hasGroup(groups, groupId) { 5615 if (!groupId || !groups) { 5616 return false;
5617 } 5618 return groups.indexOf(groupId) !== -1; 5619} 5620 5621function updateFromToPTS(fragFrom, fragTo) { 5622 const fragToPTS = fragTo.startPTS; 5623 // if we know startPTS[toIdx] 5624 if (isFiniteNumber(fragToPTS)) { 5625 // update fragment duration. 5626 // it helps to fix drifts between playlist reported duration and fragment real duration 5627 let duration = 0; 5628 let frag; 5629 if (fragTo.sn > fragFrom.sn) { 5630 duration = fragToPTS - fragFrom.start; 5631 frag = fragFrom; 5632 } else { 5633 duration = fragFrom.start - fragToPTS; 5634 frag = fragTo; 5635 } 5636 if (frag.duration !== duration) { 5637 frag.duration = duration; 5638 } 5639 // we dont know startPTS[toIdx] 5640 } else if (fragTo.sn > fragFrom.sn) { 5641 const contiguous = fragFrom.cc === fragTo.cc; 5642 // TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS 5643 if (contiguous && fragFrom.minEndPTS) { 5644 fragTo.start = fragFrom.start + (fragFrom.minEndPTS - fragFrom.start); 5645 } else { 5646 fragTo.start = fragFrom.start + fragFrom.duration; 5647 } 5648 } else { 5649 fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0); 5650 } 5651} 5652function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) { 5653 const parsedMediaDuration = endPTS - startPTS; 5654 if (parsedMediaDuration <= 0) { 5655 logger.warn('Fragment should have a positive duration', frag); 5656 endPTS = startPTS + frag.duration; 5657 endDTS = startDTS + frag.duration; 5658 } 5659 let maxStartPTS = startPTS; 5660 let minEndPTS = endPTS; 5661 const fragStartPts = frag.startPTS; 5662 const fragEndPts = frag.endPTS; 5663 if (isFiniteNumber(fragStartPts)) { 5664 // delta PTS between audio and video 5665 const deltaPTS = Math.abs(fragStartPts - startPTS); 5666 if (!isFiniteNumber(frag.deltaPTS)) { 5667 frag.deltaPTS = deltaPTS; 5668 } else { 5669 frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS); 5670 } 5671 maxStartPTS = Math.max(startPTS, fragStartPts); 5672 startPTS = Math.min(startPTS, fragStartPts); 5673 startDTS = Math.min(startDTS, frag.startDTS); 5674 minEndPTS = Math.min(endPTS, fragEndPts); 5675 endPTS = Math.max(endPTS, fragEndPts); 5676 endDTS = Math.max(endDTS, frag.endDTS); 5677 } 5678 const drift = startPTS - frag.start; 5679 if (frag.start !== 0) { 5680 frag.start = startPTS; 5681 } 5682 frag.duration = endPTS - frag.start; 5683 frag.startPTS = startPTS; 5684 frag.maxStartPTS = maxStartPTS; 5685 frag.startDTS = startDTS; 5686 frag.endPTS = endPTS; 5687 frag.minEndPTS = minEndPTS; 5688 frag.endDTS = endDTS; 5689 const sn = frag.sn; // 'initSegment' 5690 // exit if sn out of range 5691 if (!details || sn < details.startSN || sn > details.endSN) { 5692 return 0; 5693 } 5694 let i; 5695 const fragIdx = sn - details.startSN; 5696 const fragments = details.fragments; 5697 // update frag reference in fragments array 5698 // rationale is that fragments array might not contain this frag object. 5699 // this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS() 5700 // if we don't update frag, we won't be able to propagate PTS info on the playlist 5701 // resulting in invalid sliding computation 5702 fragments[fragIdx] = frag; 5703 // adjust fragment PTS/duration from seqnum-1 to frag 0 5704 for (i = fragIdx; i > 0; i--) { 5705 updateFromToPTS(fragments[i], fragments[i - 1]); 5706 } 5707 5708 // adjust fragment PTS/duration from seqnum to last frag 5709 for (i = fragIdx; i < fragments.length - 1; i++) { 5710 updateFromToPTS(fragments[i], fragments[i + 1]); 5711 } 5712 if (details.fragmentHint) { 5713 updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint); 5714 } 5715 details.PTSKnown = details.alignedSliding = true; 5716 return drift; 5717} 5718function mergeDetails(oldDetails, newDetails) { 5719 // Track the last initSegment processed. Initialize it to the last one on the timeline. 5720 let currentInitSegment = null; 5721 const oldFragments = oldDetails.fragments; 5722 for (let i = oldFragments.length - 1; i >= 0; i--) { 5723 const oldInit = oldFragments[i].initSegment; 5724 if (oldInit) { 5725 currentInitSegment = oldInit; 5726 break; 5727 } 5728 } 5729 if (oldDetails.fragmentHint) { 5730 // prevent PTS and duration from being adjusted on the next hint 5731 delete oldDetails.fragmentHint.endPTS; 5732 } 5733 // check if old/new playlists have fragments in common 5734 // loop through overlapping SN and update startPTS , cc, and duration if any foun
5734d 5735 let ccOffset = 0; 5736 let PTSFrag; 5737 mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag) => { 5738 if (oldFrag.relurl) { 5739 // Do not compare CC if the old fragment has no url. This is a level.fragmentHint used by LL-HLS parts. 5740 // It maybe be off by 1 if it was created before any parts or discontinuity tags were appended to the end 5741 // of the playlist. 5742 ccOffset = oldFrag.cc - newFrag.cc; 5743 } 5744 if (isFiniteNumber(oldFrag.startPTS) && isFiniteNumber(oldFrag.endPTS)) { 5745 newFrag.start = newFrag.startPTS = oldFrag.startPTS; 5746 newFrag.startDTS = oldFrag.startDTS; 5747 newFrag.maxStartPTS = oldFrag.maxStartPTS; 5748 newFrag.endPTS = oldFrag.endPTS; 5749 newFrag.endDTS = oldFrag.endDTS; 5750 newFrag.minEndPTS = oldFrag.minEndPTS; 5751 newFrag.duration = oldFrag.endPTS - oldFrag.startPTS; 5752 if (newFrag.duration) { 5753 PTSFrag = newFrag; 5754 } 5755 5756 // PTS is known when any segment has startPTS and endPTS 5757 newDetails.PTSKnown = newDetails.alignedSliding = true; 5758 } 5759 newFrag.elementaryStreams = oldFrag.elementaryStreams; 5760 newFrag.loader = oldFrag.loader; 5761 newFrag.stats = oldFrag.stats; 5762 if (oldFrag.initSegment) { 5763 newFrag.initSegment = oldFrag.initSegment; 5764 currentInitSegment = oldFrag.initSegment; 5765 } 5766 }); 5767 if (currentInitSegment) { 5768 const fragmentsToCheck = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments; 5769 fragmentsToCheck.forEach(frag => { 5770 var _currentInitSegment; 5771 if (frag && (!frag.initSegment || frag.initSegment.relurl === ((_currentInitSegment = currentInitSegment) == null ? void 0 : _currentInitSegment.relurl))) { 5772 frag.initSegment = currentInitSegment; 5773 } 5774 }); 5775 } 5776 if (newDetails.skippedSegments) { 5777 newDetails.deltaUpdateFailed = newDetails.fragments.some(frag => !frag); 5778 if (newDetails.deltaUpdateFailed) { 5779 logger.warn('[level-helper] Previous playlist missing segments ski
5779pped in delta playlist'); 5780 for (let i = newDetails.skippedSegments; i--;) { 5781 newDetails.fragments.shift(); 5782 } 5783 newDetails.startSN = newDetails.fragments[0].sn; 5784 newDetails.startCC = newDetails.fragments[0].cc; 5785 } else if (newDetails.canSkipDateRanges) { 5786 newDetails.dateRanges = mergeDateRanges(oldDetails.dateRanges, newDetails.dateRanges, newDetails.recentlyRemovedDateranges); 5787 } 5788 } 5789 const newFragments = newDetails.fragments; 5790 if (ccOffset) { 5791 logger.warn('discontinuity sliding from playlist, take drift into account'); 5792 for (let i = 0; i < newFragments.length; i++) { 5793 newFragments[i].cc += ccOffset; 5794 } 5795 } 5796 if (newDetails.skippedSegments) { 5797 newDetails.startCC = newDetails.fragments[0].cc; 5798 } 5799 5800 // Merge parts 5801 mapPartIntersection(oldDetails.partList, newDetails.partList, (oldPart, newPart) => { 5802 newPart.elementaryStreams = oldPart.elementaryStreams; 5803 newPart.stats = oldPart.stats; 5804 }); 5805 5806 // if at least one fragment contains PTS info, recompute PTS information for all fragments 5807 if (PTSFrag) { 5808 updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS); 5809 } else { 5810 // ensure that delta is within oldFragments range 5811 // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61]) 5812 // in that case we also need to adjust start offset of all fragments 5813 adjustSliding(oldDetails, newDetails); 5814 } 5815 if (newFragments.length) { 5816 newDetails.totalduration = newDetails.edge - newFragments[0].start; 5817 } 5818 newDetails.driftStartTime = oldDetails.driftStartTime; 5819 newDetails.driftStart = oldDetails.driftStart; 5820 const advancedDateTime = newDetails.advancedDateTime; 5821 if (newDetails.advanced && advancedDateTime) { 5822 const edge = newDetails.edge; 5823 if (!newDetails.driftStart) { 5824 newDetails.driftStartTime = advancedDateTime; 5825 newDetails.driftStart = edge; 5826 } 5827 newDetails.driftEndTime = advancedDateTime; 5828 newDetails.driftEnd = edge; 5829 } else { 5830 newDetails.driftEndTime = oldDetails.driftEndTime; 5831 newDetails.driftEnd = oldDetails.driftEnd; 5832 newDetails.advancedDateTime = oldDetails.advancedDateTime; 5833 } 5834} 5835function mergeDateRanges(oldDateRanges, deltaDateRanges, recentlyRemovedDateranges) { 5836 const dateRanges = _extends({}, oldDateRanges); 5837 if (recentlyRemovedDateranges) { 5838 recentlyRemovedDateranges.forEach(id => { 5839 delete dateRanges[id]; 5840 }); 5841 } 5842 Object.keys(deltaDateRanges).forEach(id => { 5843 const dateRange = new DateRange(deltaDateRanges[id].attr, dateRanges[id]); 5844 if (dateRange.isValid) { 5845 dateRanges[id] = dateRange; 5846 } else { 5847 logger.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(deltaDateRanges[id].attr)}"`); 5848 } 5849 }); 5850 return dateRanges; 5851} 5852function mapPartIntersection(oldParts, newParts, intersectionFn) { 5853 if (oldParts && newParts) { 5854 let delta = 0; 5855 for (let i = 0, len = oldParts.length; i <= len; i++) { 5856 const oldPart = oldParts[i]; 5857 const newPart = newParts[i + delta]; 5858 if (oldPart && newPart && oldPart.index === newPart.index && oldPart.fragment.sn === newPart.fragment.sn) { 5859 intersectionFn(oldPart, newPart); 5860 } else { 5861 delta--; 5862 } 5863 } 5864 } 5865} 5866function mapFragmentIntersection(oldDetails, newDetails, intersectionFn) { 5867 const skippedSegments = newDetails.skippedSegments; 5868 const start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN; 5869 const end = (oldDetails.fragmentHint ? 1 : 0) + (skippedSegments ? newDetails.endSN : Math.min(oldDetails.endSN, newDetails.endSN)) - newDetails.startSN; 5870 const delta = newDetails.startSN - oldDetails.startSN; 5871 const newFrags = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments; 5872 const oldFrags = oldDetails.fragmentHint ? oldDetails.fragments.concat(oldDetails.fragmentHint) : oldDetails.fragments; 5873 for (let i = start; i <= end; i++) { 5874 const oldFrag = oldFrags[delta + i]; 5875 let newFrag = newFrags[i];
5876 if (skippedSegments && !newFrag && i < skippedSegments) { 5877 // Fill in skipped segments in delta playlist 5878 newFrag = newDetails.fragments[i] = oldFrag; 5879 } 5880 if (oldFrag && newFrag) { 5881 intersectionFn(oldFrag, newFrag); 5882 } 5883 } 5884} 5885function adjustSliding(oldDetails, newDetails) { 5886 const delta = newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN; 5887 const oldFragments = oldDetails.fragments; 5888 if (delta < 0 || delta >= oldFragments.length) { 5889 return; 5890 } 5891 addSliding(newDetails, oldFragments[delta].start); 5892} 5893function addSliding(details, start) { 5894 if (start) { 5895 const fragments = details.fragments; 5896 for (let i = details.skippedSegments; i < fragments.length; i++) { 5897 fragments[i].start += start; 5898 } 5899 if (details.fragmentHint) { 5900 details.fragmentHint.start += start; 5901 } 5902 } 5903} 5904function computeReloadInterval(newDetails, distanceToLiveEdgeMs = Infinity) { 5905 let reloadInterval = 1000 * newDetails.targetduration; 5906 if (newDetails.updated) { 5907 // Use last segment duration when shorter than target duration and near live edge 5908 const fragments = newDetails.fragments; 5909 const liveEdgeMaxTargetDurations = 4; 5910 if (fragments.length && reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs) { 5911 const lastSegmentDuration = fragments[fragments.length - 1].duration * 1000; 5912 if (lastSegmentDuration < reloadInterval) { 5913 reloadInterval = lastSegmentDuration; 5914 } 5915 } 5916 } else { 5917 // estimate = 'miss half average'; 5918 // follow HLS Spec, If the client reloads a Playlist file and finds that it has not 5919 // changed then it MUST wait for a period of one-half the target 5920 // duration before retrying. 5921 reloadInterval /= 2; 5922 } 5923 return Math.round(reloadInterval); 5924} 5925function getFragmentWithSN(level, sn, fragCurrent) { 5926 if (!(level != null && level.details)) { 5927 return null; 5928 } 5929 const levelDetails = level.details; 5930 let fragment = levelDetails.fragments[sn - levelDetails.startSN]; 5931 if (fragment) { 5932 return fragment; 5933 } 5934 fragment = levelDetails.fragmentHint; 5935 if (fragment && fragment.sn === sn) { 5936 return fragment; 5937 } 5938 if (sn < levelDetails.startSN && fragCurrent && fragCurrent.sn === sn) { 5939 return fragCurrent; 5940 } 5941 return null; 5942} 5943function getPartWith(level, sn, partIndex) { 5944 var _level$details; 5945 if (!(level != null && level.details)) { 5946 return null; 5947 } 5948 return findPart((_level$details = level.details) == null ? void 0 : _level$details.partList, sn, partIndex); 5949} 5950function findPart(partList, sn, partIndex) { 5951 if (partList) { 5952 for (let i = partList.length; i--;) { 5953 const part = partList[i]; 5954 if (part.index === partIndex && part.fragment.sn === sn) { 5955 return part; 5956 } 5957 } 5958 } 5959 return null; 5960} 5961function reassignFragmentLevelIndexes(levels) { 5962 levels.forEach((level, index) => { 5963 const { 5964 details 5965 } = level; 5966 if (details != null && details.fragments) { 5967 details.fragments.forEach(fragment => { 5968 fragment.level = index; 5969 }); 5970 } 5971 }); 5972} 5973 5974function isTimeoutError(error) { 5975 switch (error.details) { 5976 case ErrorDetails.FRAG_LOAD_TIMEOUT: 5977 case ErrorDetails.KEY_LOAD_TIMEOUT: 5978 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 5979 case ErrorDetails.MANIFEST_LOAD_TIMEOUT: 5980 return true; 5981 } 5982 return false; 5983} 5984function getRetryConfig(loadPolicy, error) { 5985 const isTimeout = isTimeoutError(error); 5986 return loadPolicy.default[`${isTimeout ? 'timeout' : 'error'}Retry`]; 5987} 5988function getRetryDelay(retryConfig, retryCount) { 5989 // exponential backoff capped to max retry delay 5990 const backoffFactor = retryConfig.backoff === 'linear' ? 1 : Math.pow(2, retryCount); 5991 return Math.min(backoffFactor * retryConfig.retryDelayMs, retryConfig.maxRetryDelayMs); 5992} 5993function getLoaderConfigWithoutReties(loderConfig) { 5994 return _objectSpread2(_objectSpread2({}, loderConfig), { 5995 errorRetry: null, 5996 timeoutRetry: null 5997 }); 5998} 5999function shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse) { 6000 if (!retryConfig) { 6001 return false; 6002 } 6003 const httpStatus = loaderResponse == null ? void 0 : loaderResponse.code; 6004 const retry = retryCount < retryConfig.maxNumRetry && (retryForHttpStatus(httpStatus) || !!isTimeout); 6005 return retryConfig.shouldRetry ? retryConfig.shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse, retry) : retry; 6006} 6007function retryForHttpStatus(httpStatus) {
6008 // Do not retry on status 4xx, status 0 (CORS error), or undefined (decrypt/gap/parse error) 6009 return httpStatus === 0 && navigator.onLine === false || !!httpStatus && (httpStatus < 400 || httpStatus > 499); 6010} 6011 6012const BinarySearch = { 6013 /** 6014 * Searches for an item in an array which matches a certain condition. 6015 * This requires the condition to only match one item in the array, 6016 * and for the array to be ordered. 6017 * 6018 * @param list The array to search. 6019 * @param comparisonFn 6020 * Called and provided a candidate item as the first argument. 6021 * Should return: 6022 * > -1 if the item should be located at a lower index than the provided item. 6023 * > 1 if the item should be located at a higher index than the provided item. 6024 * > 0 if the item is the item you're looking for. 6025 * 6026 * @returns the object if found, otherwise returns null 6027 */ 6028 search: function (list, comparisonFn) { 6029 let minIndex = 0; 6030 let maxIndex = list.length - 1; 6031 let currentIndex = null; 6032 let currentElement = null; 6033 while (minIndex <= maxIndex) { 6034 currentIndex = (minIndex + maxIndex) / 2 | 0; 6035 currentElement = list[currentIndex]; 6036 const comparisonResult = comparisonFn(currentElement); 6037 if (comparisonResult > 0) { 6038 minIndex = currentIndex + 1; 6039 } else if (comparisonResult < 0) { 6040 maxIndex = currentIndex - 1; 6041 } else { 6042 return currentElement; 6043 } 6044 } 6045 return null; 6046 } 6047}; 6048 6049/** 6050 * Returns first fragment whose endPdt value exceeds the given PDT, or null. 6051 * @param fragments - The array of candidate fragments 6052 * @param PDTValue - The PDT value which must be exceeded 6053 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous 6054 */ 6055function findFragmentByPDT(fragments, PDTValue, maxFragLookUpTolerance) { 6056 if (PDTValue === null || !Array.isArray(fragments) || !fragments.length || !isFiniteNumber(PDTValue)) { 6057 return null; 6058 } 6059 6060 // if less than start 6061 const startPDT = fragments[0].programDateTime; 6062 if (PDTValue < (startPDT || 0)) { 6063 return null; 6064 } 6065 const endPDT = fragments[fragments.length - 1].endProgramDateTime; 6066 if (PDTValue >= (endPDT || 0)) { 6067 return null; 6068 } 6069 maxFragLookUpTolerance = maxFragLookUpTolerance || 0; 6070 for (let seg = 0; seg < fragments.length; ++seg) { 6071 const frag = fragments[seg]; 6072 if (pdtWithinToleranceTest(PDTValue, maxFragLookUpTolerance, frag)) { 6073 return frag; 6074 } 6075 } 6076 return null; 6077} 6078 6079/** 6080 * Finds a fragment based on the SN of the previous fragment; or based on the needs of the current buffer. 6081 * This method compensates for small buffer gaps by applying a tolerance to the start of any candidate fragment, thus 6082 * breaking any traps which would cause the same fragment to be continuously selected within a small range. 6083 * @param fragPrevious - The last frag successfully appended 6084 * @param fragments - The array of candidate fragments 6085 * @param bufferEnd - The end of the contiguous buffered range the playhead is currently within 6086 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous 6087 * @returns a matching fragment or null 6088 */ 6089function findFragmentByPTS(fragPrevious, fragments, bufferEnd = 0, maxFragLookUpTolerance = 0, nextFragLookupTolerance = 0.005) { 6090 let fragNext = null; 6091 if (fragPrevious) { 6092 fragNext = fragments[fragPrevious.sn - fragments[0].sn + 1] || null; 6093 // check for buffer-end rounding error 6094 const bufferEdgeError = fragPrevious.endDTS - bufferEnd; 6095 if (bufferEdgeError > 0 && bufferEdgeError < 0.0000015) { 6096 bufferEnd += 0.0000015; 6097 } 6098 } else if (bufferEnd === 0 && fragments[0].start === 0) { 6099 fragNext = fragments[0]; 6100 } 6101 // Prefer the next fragment if it's within tolerance 6102 if (fragNext && ((!fragPrevious || fragPrevious.level === fragNext.level) && fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext) === 0 || fragmentWithinFastStartSwitch(fragNext, fragPrevious, Math.min(nextFragLookupTolerance, maxFragLookUpTolerance)))) { 6103 return fragNext; 6104 } 6105 // We might be seeking past the tolerance so find the best match 6106 const foundFragment = BinarySearch.search(fragments, fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance)); 6107 if (foundFragment && (foundFragment !== fragPrevious || !fragNext)) { 6108 return foundFragment; 6109 } 6110 // If no match was found return the next fragment after fragPrevious, or null 6111 return fragNext; 6112} 6113function fragmentWithinFastStartSwitch(fragNext, fragPrevious, nextFragLookupTolerance) { 6114 if (fragPrevious && fragPrevious.start === 0 && fragPrevious.level < fragNext.level && (fragPrevious.endPTS || 0) > 0) { 6115 const firstDuration = fragPrevious.tagList.reduce((duration, tag) => { 6116 if (tag[0] === 'INF') { 6117 duration += parseFloat(tag[1]); 6118 } 6119 return duration; 6120 }, nextFragLookupTolerance); 6121 return fragNext.start <= firstDuration; 6122 } 6123 return false;
6124} 6125 6126/** 6127 * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions. 6128 * @param candidate - The fragment to test 6129 * @param bufferEnd - The end of the current buffered range the playhead is currently within 6130 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous 6131 * @returns 0 if it matches, 1 if too low, -1 if too high 6132 */ 6133function fragmentWithinToleranceTest(bufferEnd = 0, maxFragLookUpTolerance = 0, candidate) { 6134 // eagerly accept an accurate match (no tolerance) 6135 if (candidate.start <= bufferEnd && candidate.start + candidate.duration > bufferEnd) { 6136 return 0; 6137 } 6138 // offset should be within fragment boundary - config.maxFragLookUpTolerance 6139 // this is to cope with situations like 6140 // bufferEnd = 9.991 6141 // frag[Ø] : [0,10] 6142 // frag[1] : [10,20] 6143 // bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here 6144 // frag start frag start+duration 6145 // |-----------------------------| 6146 // <---> <---> 6147 // ...--------><-----------------------------><---------.... 6148 // previous frag matching fragment next frag 6149 // return -1 return 0 return 1 6150 // logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`); 6151 // Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments 6152 const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)); 6153 if (candidate.start + candidate.duration - candidateLookupTolerance <= bufferEnd) { 6154 return 1; 6155 } else if (candidate.start - candidateLookupTolerance > bufferEnd && candidate.start) { 6156 // if maxFragLookUpTolerance will have negative value then don't return -1 for first element 6157 return -1; 6158 } 6159 return 0; 6160} 6161 6162/** 6163 * The test function used by the findFragmentByPdt's BinarySearch to look for the best match to the current buffer conditions. 6164 * This function tests the candidate's program date time values, as represented in Unix time 6165 * @param candidate - The fragment to test 6166 * @param pdtBufferEnd - The Unix time representing the end of the current buffered range 6167 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous 6168 * @returns true if contiguous, false otherwise 6169 */ 6170function pdtWithinToleranceTest(pdtBufferEnd, maxFragLookUpTolerance, candidate) { 6171 const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)) * 1000; 6172 6173 // endProgramDateTime can be null, default to zero 6174 const endProgramDateTime = candidate.endProgramDateTime || 0; 6175 return endProgramDateTime - candidateLookupTolerance > pdtBufferEnd; 6176} 6177function findFragWithCC(fragments, cc) { 6178 return BinarySearch.search(fragments, candidate => { 6179 if (candidate.cc < cc) { 6180 return 1; 6181 } else if (candidate.cc > cc) { 6182 return -1; 6183 } else { 6184 return 0; 6185 } 6186 }); 6187} 6188 6189var NetworkErrorAction = { 6190 DoNothing: 0, 6191 SendAlternateToPenaltyBox: 2, 6192 RemoveAlternatePermanently: 3, 6193 RetryRequest: 5 6194}; 6195var ErrorActionFlags = { 6196 None: 0, 6197 MoveAllAlternatesMatchingHost: 1, 6198 MoveAllAlternatesMatchingHDCP: 2}; // Reserved for future use 6199class ErrorController { 6200 constructor(hls) { 6201 this.hls = void 0; 6202 this.playlistError = 0; 6203 this.penalizedRenditions = {}; 6204 this.log = void 0; 6205 this.warn = void 0; 6206 this.error = void 0; 6207 this.hls = hls; 6208 this.log = logger.log.bind(logger, `[info]:`); 6209 this.warn = logger.warn.bind(logger, `[warning]:`); 6210 this.error = logger.error.bind(logger, `[error]:`); 6211 this.registerListeners(); 6212 } 6213 registerListeners() { 6214 const hls = this.hls; 6215 hls.on(Events.ERROR, this.onError, this); 6216 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 6217 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 6218 } 6219 unregisterListeners() { 6220 const hls = this.hls; 6221 if (!hls) { 6222 return; 6223 } 6224 hls.off(Events.ERROR, this.onError, this); 6225 hls.off(Events.ERROR, this.onErrorOut, this); 6226 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 6227 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 6228 } 6229 destroy() { 6230 this.unregisterListeners(); 6231 // @ts-ignore 6232 this.hls = null; 6233 this.penalizedRenditions = {}; 6234 } 6235 startLoad(startPosition) {} 6236 stopLoad() { 6237 this.playlistError = 0; 6238 } 6239 getVariantLevelIndex(frag) { 6240 return (frag == null ? void 0 : frag.type) === PlaylistLevelType.MAIN ? frag.level : this.hls.loadLevel; 6241 } 6242 onManifestLoading() { 6243 this.playlistError = 0; 6244 this.penalizedRenditions = {}; 6245 } 6246 onLevelUpdated() { 6247 this.playlistError = 0; 6248 } 6249 onError(event, data) { 6250 var _data$frag, _data$level; 6251 if (data.fatal) { 6252 return; 6253 } 6254 const hls = this.hls; 6255 const context = data.context; 6256 switch (data.details) { 6257 case ErrorDetails.FRAG_LOAD_ERROR: 6258 case ErrorDetails.FRAG_LOAD_TIMEOUT: 6259 case ErrorDetails.KEY_LOAD_ERROR: 6260 case ErrorDetails.KEY_LOAD_TIMEOUT: 6261 data.errorAction = this.getFragRetryOrSwitchAction(data); 6262 return; 6263 case ErrorDetails.FRAG_PARSING_ERROR: 6264 // ignore empty segment errors marked as gap 6265 if ((_data$frag = data.frag) != null && _data$frag.gap) { 6266 data.errorAction = { 6267 action: NetworkErrorAction.DoNothing, 6268 flags: ErrorActionFlags.None 6269 }; 6270 return; 6271 } 6272 // falls through 6273 case ErrorDetails.FRAG_GAP: 6274 case ErrorDetails.FRAG_DECRYPT_ERROR: 6275 { 6276 // Switch level if possible, otherwise allow retry count to reach max error retries 6277 data.errorAction = this.getFragRetryOrSwitchAction(data); 6278 data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; 6279 return; 6280 } 6281 case ErrorDetails.LEVEL_EMPTY_ERROR: 6282 case ErrorDetails.LEVEL_PARSING_ERROR: 6283 { 6284 var _data$context, _data$context$levelDe; 6285 // Only retry when empty and live 6286 const levelIndex = data.parent === PlaylistLevelType.MAIN ? data.level : hls.loadLevel; 6287 if (data.details === ErrorDetails.LEVEL_EMPTY_ERROR && !!((_data$context = data.context) != null && (_data$context$levelDe = _data$context.levelDetails) != null && _data$context$levelDe.live)) { 6288 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, levelIndex); 6289 } else { 6290 // Escalate to fatal if not retrying or switching 6291 data.levelRetry = false;
6292 data.errorAction = this.getLevelSwitchAction(data, levelIndex); 6293 } 6294 } 6295 return; 6296 case ErrorDetails.LEVEL_LOAD_ERROR: 6297 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 6298 if (typeof (context == null ? void 0 : context.level) === 'number') { 6299 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, context.level); 6300 } 6301 return; 6302 case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: 6303 case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: 6304 case ErrorDetails.SUBTITLE_LOAD_ERROR: 6305 case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT: 6306 if (context) { 6307 const level = hls.levels[hls.loadLevel]; 6308 if (level && (context.type === PlaylistContextType.AUDIO_TRACK && level.hasAudioGroup(context.groupId) || context.type === PlaylistContextType.SUBTITLE_TRACK && level.hasSubtitleGroup(context.groupId))) { 6309 // Perform Pathway switch or Redundant failover if possible for fastest recovery 6310 // otherwise allow playlist retry count to reach max error retries 6311 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, hls.loadLevel); 6312 data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; 6313 data.errorAction.flags = ErrorActionFlags.MoveAllAlternatesMatchingHost; 6314 return; 6315 } 6316 } 6317 return; 6318 case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: 6319 { 6320 const level = hls.levels[hls.loadLevel]; 6321 const restrictedHdcpLevel = level == null ? void 0 : level.attrs['HDCP-LEVEL']; 6322 if (restrictedHdcpLevel) { 6323 data.errorAction = { 6324 action: NetworkErrorAction.SendAlternateToPenaltyBox, 6325 flags: ErrorActionFlags.MoveAllAlternatesMatchingHDCP, 6326 hdcpLevel: restrictedHdcpLevel 6327 }; 6328 } else { 6329 this.keySystemError(data); 6330 } 6331 } 6332 return; 6333 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 6334 case ErrorDetails.REMUX_ALLOC_ERROR: 6335 case ErrorDetails.BUFFER_APPEND_ERROR: 6336 data.errorAction = this.getLevelSwitchAction(data, (_data$level = data.level) != null ? _data$level : hls.loadLevel); 6337 return; 6338 case ErrorDetails.INTERNAL_EXCEPTION: 6339 case ErrorDetails.BUFFER_APPENDING_ERROR: 6340 case ErrorDetails.BUFFER_FULL_ERROR: 6341 case ErrorDetails.LEVEL_SWITCH_ERROR: 6342 case ErrorDetails.BUFFER_STALLED_ERROR: 6343 case ErrorDetails.BUFFER_SEEK_OVER_HOLE: 6344 case ErrorDetails.BUFFER_NUDGE_ON_STALL: 6345 data.errorAction = { 6346 action: NetworkErrorAction.DoNothing, 6347 flags: ErrorActionFlags.None 6348 }; 6349 return; 6350 } 6351 if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) { 6352 this.keySystemError(data); 6353 } 6354 } 6355 keySystemError(data) { 6356 const levelIndex = this.getVariantLevelIndex(data.frag); 6357 // Do not retry level. Escalate to fatal if switching levels fails. 6358 data.levelRetry = false; 6359 data.errorAction = this.getLevelSwitchAction(data, levelIndex); 6360 } 6361 getPlaylistRetryOrSwitchAction(data, levelIndex) { 6362 const hls = this.hls; 6363 const retryConfig = getRetryConfig(hls.config.playlistLoadPolicy, data); 6364 const retryCount = this.playlistError++; 6365 const retry = shouldRetry(retryConfig, retryCount, isTimeoutError(data), data.response); 6366 if (retry) { 6367 return { 6368 action: NetworkErrorAction.RetryRequest, 6369 flags: ErrorActionFlags.None, 6370 retryConfig, 6371 retryCount 6372 }; 6373 } 6374 const errorAction = this.getLevelSwitchAction(data, levelIndex); 6375 if (retryConfig) { 6376 errorAction.retryConfig = retryConfig; 6377 errorAction.retryCount = retryCount; 6378 } 6379 return errorAction; 6380 } 6381 getFragRetryOrSwitchAction(data) { 6382 const hls = this.hls; 6383 // Share fragment error count accross media options (main, audio, subs) 6384 // This allows for level based rendition switching when media option assets fail 6385 const variantLevelIndex = this.getVariantLevelIndex(data.frag); 6386 const level = hls.levels[variantLevelIndex]; 6387 const { 6388 fragLoadPolicy, 6389 keyLoadPolicy 6390 } = hls.config; 6391 const retryConfig = getRetryConfig(data.details.startsWith('key') ? keyLoadPolicy : fragLoadPolicy, data); 6392 const fragmentErrors = hls.levels.reduce((acc, level) => acc + level.fragmentError, 0); 6393 // Switch levels when out of retried or level index out of bounds 6394 if (level) { 6395 if (data.details !== ErrorDetails.FRAG_GAP) { 6396 level.fragmentError++; 6397 } 6398 const retry = shouldRetry(retryConfig, fragmentErrors, isTimeoutError(data), data.response); 6399 if (retry) { 6400 return { 6401 action: NetworkErrorAction.RetryRequest, 6402 flags: ErrorActionFlags.None, 6403 retryConfig, 6404 retryCount: fragmentErrors 6405 }; 6406 } 6407 } 6408 // Reach max retry count, or Missing level reference 6409 // Switch to valid index 6410 const errorAction = this.getLevelSwitchAction(data, variantLevelIndex); 6411 // Add retry details to allow skipping of FRAG_PARSING_ERROR 6412 if (retryConfig) { 6413 errorAction.retryConfig = retryConfig; 6414 errorAction.retryCount = fragmentErrors; 6415 } 6416 return errorAction; 6417 } 6418 getLevelSwitchAction(data, levelIndex) { 6419 const hls = this.hls; 6420 if (levelIndex === null || levelIndex === undefined) { 6421 levelIndex = hls.loadLevel; 6422 } 6423 const level = this.hls.levels[levelIndex]; 6424 if (level) { 6425 var _data$frag2, _data$context2; 6426 const errorDetails = data.details; 6427 level.loadError++; 6428 if (errorDetails === ErrorDetails.BUFFER_APPEND_ERROR) { 6429 level.fragmentError++; 6430 } 6431 // Search for next level to retry 6432 let nextLevel = -1; 6433 const { 6434 levels, 6435 loadLevel, 6436 minAutoLevel, 6437 maxAutoLevel 6438 } = hls; 6439 if (!hls.autoLevelEnabled) { 6440 hls.loadLevel = -1; 6441 } 6442 const fragErrorType = (_data$frag2 = data.frag) == null ? void 0 : _data$frag2.type; 6443 // Find alternate audio codec if available on audio codec error 6444 const isAudioCodecError = fragErrorType === PlaylistLevelType.AUDIO && errorDetails === ErrorDetails.FRAG_PARSING_ERROR || data.sourceBufferName === 'audio' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); 6445 const findAudioCodecAlternate = isAudioCodecError && levels.some(({ 6446 audioCodec 6447 }) => level.audioCodec !== audioCodec); 6448 // Find alternate video codec if available on video codec error 6449 const isVideoCodecError = data.sourceBufferName === 'video' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); 6450 const findVideoCodecAlternate = isVideoCodecError && levels.some(({ 6451 codecSet, 6452 audioCodec 6453 }) =>
vendor: 4,339 bytes, lines 6453-6542
6453 level.codecSet !== codecSet && level.audioCodec === audioCodec); 6454 const { 6455 type: playlistErrorType, 6456 groupId: playlistErrorGroupId 6457 } = (_data$context2 = data.context) != null ? _data$context2 : {}; 6458 for (let i = levels.length; i--;) { 6459 const candidate = (i + loadLevel) % levels.length; 6460 if (candidate !== loadLevel && candidate >= minAutoLevel && candidate <= maxAutoLevel && levels[candidate].loadError === 0) { 6461 var _level$audioGroups, _level$subtitleGroups; 6462 const levelCandidate = levels[candidate]; 6463 // Skip level switch if GAP tag is found in next level at same position 6464 if (errorDetails === ErrorDetails.FRAG_GAP && fragErrorType === PlaylistLevelType.MAIN && data.frag) { 6465 const levelDetails = levels[candidate].details; 6466 if (levelDetails) { 6467 const fragCandidate = findFragmentByPTS(data.frag, levelDetails.fragments, data.frag.start); 6468 if (fragCandidate != null && fragCandidate.gap) { 6469 continue; 6470 } 6471 } 6472 } else if (playlistErrorType === PlaylistContextType.AUDIO_TRACK && levelCandidate.hasAudioGroup(playlistErrorGroupId) || playlistErrorType === PlaylistContextType.SUBTITLE_TRACK && levelCandidate.hasSubtitleGroup(playlistErrorGroupId)) { 6473 // For audio/subs playlist errors find another group ID or fallthrough to redundant fail-over 6474 continue; 6475 } else if (fragErrorType === PlaylistLevelType.AUDIO && (_level$audioGroups = level.audioGroups) != null && _level$audioGroups.some(groupId => levelCandidate.hasAudioGroup(groupId)) || fragErrorType === PlaylistLevelType.SUBTITLE && (_level$subtitleGroups = level.subtitleGroups) != null && _level$subtitleGroups.some(groupId => levelCandidate.hasSubtitleGroup(groupId)) || findAudioCodecAlternate && level.audioCodec === levelCandidate.audioCodec || !findAudioCodecAlternate && level.audioCodec !== levelCandidate.audioCodec || findVideoCodecAlternate && level.codecSet === levelCandidate.codecSet) { 6476 // For video/audio/subs frag errors find another group ID or fallthrough to redundant fail-over 6477 continue; 6478 } 6479 nextLevel = candidate; 6480 break; 6481 } 6482 } 6483 if (nextLevel > -1 && hls.loadLevel !== nextLevel) { 6484 data.levelRetry = true; 6485 this.playlistError = 0; 6486 return { 6487 action: NetworkErrorAction.SendAlternateToPenaltyBox, 6488 flags: ErrorActionFlags.None, 6489 nextAutoLevel: nextLevel 6490 }; 6491 } 6492 } 6493 // No levels to switch / Manual level selection / Level not found 6494 // Resolve with Pathway switch, Redundant fail-over, or stay on lowest Level 6495 return { 6496 action: NetworkErrorAction.SendAlternateToPenaltyBox, 6497 flags: ErrorActionFlags.MoveAllAlternatesMatchingHost 6498 }; 6499 } 6500 onErrorOut(event, data) { 6501 var _data$errorAction; 6502 switch ((_data$errorAction = data.errorAction) == null ? void 0 : _data$errorAction.action) { 6503 case NetworkErrorAction.DoNothing: 6504 break; 6505 case NetworkErrorAction.SendAlternateToPenaltyBox: 6506 this.sendAlternateToPenaltyBox(data); 6507 if (!data.errorAction.resolved && data.details !== ErrorDetails.FRAG_GAP) { 6508 data.fatal = true; 6509 } else if (/MediaSource readyState: ended/.test(data.error.message)) { 6510 this.warn(`MediaSource ended after "${data.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`); 6511 this.hls.recoverMediaError(); 6512 } 6513 break; 6514 } 6515 if (data.fatal) { 6516 this.hls.stopLoad(); 6517 return; 6518 } 6519 } 6520 sendAlternateToPenaltyBox(data) { 6521 const hls = this.hls; 6522 const errorAction = data.errorAction; 6523 if (!errorAction) { 6524 return; 6525 } 6526 const { 6527 flags, 6528 hdcpLevel, 6529 nextAutoLevel 6530 } = errorAction; 6531 switch (flags) { 6532 case ErrorActionFlags.None: 6533 this.switchLevel(data, nextAutoLevel); 6534 break; 6535 case ErrorActionFlags.MoveAllAlternatesMatchingHDCP: 6536 if (hdcpLevel) { 6537 hls.maxHdcpLevel = HdcpLevels[HdcpLevels.indexOf(hdcpLevel) - 1]; 6538 errorAction.resolved = true; 6539 } 6540 this.warn(`Restricting playback to HDCP-LEVEL of "${hls.maxHdcpLevel}" or lower`); 6541 break; 6542 }
6543 // If not resolved by previous actions try to switch to next level 6544 if (!errorAction.resolved) { 6545 this.switchLevel(data, nextAutoLevel); 6546 } 6547 } 6548 switchLevel(data, levelIndex) { 6549 if (levelIndex !== undefined && data.errorAction) { 6550 this.warn(`switching to level ${levelIndex} after ${data.details}`); 6551 this.hls.nextAutoLevel = levelIndex; 6552 data.errorAction.resolved = true; 6553 // Stream controller is responsible for this but won't switch on false start 6554 this.hls.nextLoadLevel = this.hls.nextAutoLevel; 6555 } 6556 } 6557} 6558 6559class BasePlaylistController { 6560 constructor(hls, logPrefix) { 6561 this.hls = void 0; 6562 this.timer = -1; 6563 this.requestScheduled = -1; 6564 this.canLoad = false; 6565 this.log = void 0; 6566 this.warn = void 0; 6567 this.log = logger.log.bind(logger, `${logPrefix}:`); 6568 this.warn = logger.warn.bind(logger, `${logPrefix}:`); 6569 this.hls = hls; 6570 } 6571 destroy() { 6572 this.clearTimer(); 6573 // @ts-ignore 6574 this.hls = this.log = this.warn = null; 6575 } 6576 clearTimer() { 6577 if (this.timer !== -1) { 6578 self.clearTimeout(this.timer); 6579 this.timer = -1; 6580 } 6581 } 6582 startLoad() { 6583 this.canLoad = true; 6584 this.requestScheduled = -1; 6585 this.loadPlaylist(); 6586 } 6587 stopLoad() { 6588 this.canLoad = false; 6589 this.clearTimer(); 6590 } 6591 switchParams(playlistUri, previous, current) { 6592 const renditionReports = previous == null ? void 0 : previous.renditionReports; 6593 if (renditionReports) { 6594 let foundIndex = -1; 6595 for (let i = 0; i < renditionReports.length; i++) { 6596 const attr = renditionReports[i]; 6597 let uri; 6598 try { 6599 uri = new self.URL(attr.URI, previous.url).href; 6600 } catch (error) { 6601 logger.warn(`Could not construct new URL for Rendition Report: ${error}`); 6602 uri = attr.URI || ''; 6603 } 6604 // Use exact match. Otherwise, the last partial match, if any, will be used 6605 // (Playlist URI includes a query string that the Rendition Report does not) 6606 if (uri === playlistUri) { 6607 foundIndex = i; 6608 break; 6609 } else if (uri === playlistUri.substring(0, uri.length)) { 6610 foundIndex = i; 6611 } 6612 } 6613 if (foundIndex !== -1) { 6614 const attr = renditionReports[foundIndex]; 6615 const msn = parseInt(attr['LAST-MSN']) || (previous == null ? void 0 : previous.lastPartSn); 6616 let part = parseInt(attr['LAST-PART']) || (previous == null ? void 0 : previous.lastPartIndex); 6617 if (this.hls.config.lowLatencyMode) { 6618 const currentGoal = Math.min(previous.age - previous.partTarget, previous.targetduration); 6619 if (part >= 0 && currentGoal > previous.partTarget) { 6620 part += 1; 6621 } 6622 } 6623 const skip = current && getSkipValue(current); 6624 return new HlsUrlParameters(msn, part >= 0 ? part : undefined, skip); 6625 } 6626 } 6627 } 6628 loadPlaylist(hlsUrlParameters) { 6629 if (this.requestScheduled === -1) { 6630 this.requestScheduled = self.performance.now(); 6631 } 6632 // Loading is handled by the subclasses 6633 } 6634 shouldLoadPlaylist(playlist) { 6635 return this.canLoad && !!playlist && !!playlist.url && (!playlist.details || playlist.details.live); 6636 } 6637 shouldReloadPlaylist(playlist) { 6638 return this.timer === -1 && this.requestScheduled === -1 && this.shouldLoadPlaylist(playlist); 6639 } 6640 playlistLoaded(index, data, previousDetails) { 6641 const { 6642 details, 6643 stats 6644 } = data; 6645 6646 // Set last updated date-time 6647 const now = self.performance.now(); 6648 const elapsed = stats.loading.first ? Math.max(0, now - stats.loading.first) : 0; 6649 details.advancedDateTime = Date.now() - elapsed; 6650 6651 // if current playlist is a live playlist, arm a timer to reload it 6652 if (details.live || previousDetails != null && previousDetails.live) { 6653 details.reloaded(previousDetails); 6654 if (previousDetails) { 6655 this.log(`live playlist ${index} ${details.advanced ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex : details.updated ? 'UPDATED' : 'MISSED'}`); 6656 } 6657 // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments 6658 if (previousDetails && details.fragments.length > 0) { 6659 mergeDetails(previousDetails, details); 6660 } 6661 if (!this.canLoad || !details.live) { 6662 return; 6663 } 6664 let deliveryDirectives; 6665 let msn = undefined; 6666 let part = undefined; 6667 if (details.canBlockReload && details.endSN && details.advanced) { 6668 // Load level with LL-HLS delivery directives 6669 const lowLatencyMode = this.hls.config.lowLatencyMode; 6670 const lastPartSn = details.lastPartSn; 6671 const endSn = details.endSN; 6672 const lastPartIndex = details.lastPartIndex; 6673 const hasParts = lastPartIndex !== -1; 6674 const lastPart = lastPartSn === endSn; 6675 // When low latency mode is disabled, we'll skip part requests once the last part index is found 6676 const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex; 6677 if (hasParts) { 6678 msn = lastPart ? endSn + 1 : lastPartSn; 6679 part = lastPart ? nextSnStartIndex : lastPartIndex + 1; 6680 } else { 6681 msn = endSn + 1; 6682 } 6683 // Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part 6684 // Update directives to obtain the Playlist that has the estimated additional duration of media 6685 const lastAdvanced = details.age; 6686 const cdnAge = lastAdvanced + details.ageHeader; 6687 let currentGoal = Math.min(cdnAge - details.partTarget, details.targetduration * 1.5); 6688 if (currentGoal > 0) { 6689 if (previousDetails && currentGoal > previousDetails.tuneInGoal) { 6690 // If we attempted to get the next or latest playlist update, but currentGoal increased, 6691 // then we either can't catchup, or the "age" header cannot be trusted. 6692 this.warn(`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`); 6693 currentGoal = 0; 6694 } else { 6695 const segments = Math.floor(currentGoal / details.targetduration); 6696 msn += segments; 6697 if (part !== undefined) { 6698 const parts = Math.round(currentGoal % details.targetduration / details.partTarget); 6699 part += parts; 6700 } 6701 this.log(`CDN Tune-in age: ${details.ageHeader}s last advanced ${lastAdvanced.toFixed(2)}s goal: ${currentGoal} skip sn ${segments} to part ${part}`); 6702 } 6703 details.tuneInGoal = currentGoal; 6704 }
6705 deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); 6706 if (lowLatencyMode || !lastPart) { 6707 this.loadPlaylist(deliveryDirectives); 6708 return; 6709 } 6710 } else if (details.canBlockReload || details.canSkipUntil) { 6711 deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); 6712 } 6713 const bufferInfo = this.hls.mainForwardBufferInfo; 6714 const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0; 6715 const distanceToLiveEdgeMs = (details.edge - position) * 1000; 6716 const reloadInterval = computeReloadInterval(details, distanceToLiveEdgeMs); 6717 if (details.updated && now > this.requestScheduled + reloadInterval) { 6718 this.requestScheduled = stats.loading.start; 6719 } 6720 if (msn !== undefined && details.canBlockReload) { 6721 this.requestScheduled = stats.loading.first + reloadInterval - (details.partTarget * 1000 || 1000); 6722 } else if (this.requestScheduled === -1 || this.requestScheduled + reloadInterval < now) { 6723 this.requestScheduled = now; 6724 } else if (this.requestScheduled - now <= 0) { 6725 this.requestScheduled += reloadInterval; 6726 } 6727 let estimatedTimeUntilUpdate = this.requestScheduled - now; 6728 estimatedTimeUntilUpdate = Math.max(0, estimatedTimeUntilUpdate); 6729 this.log(`reload live playlist ${index} in ${Math.round(estimatedTimeUntilUpdate)} ms`); 6730 // this.log( 6731 // `live reload ${details.updated ? 'REFRESHED' : 'MISSED'} 6732 // reload in ${estimatedTimeUntilUpdate / 1000} 6733 // round trip ${(stats.loading.end - stats.loading.start) / 1000} 6734 // diff ${ 6735 // (reloadInterval - 6736 // (estimatedTimeUntilUpdate + 6737 // stats.loading.end - 6738 // stats.loading.start)) / 6739 // 1000 6740 // } 6741 // reload interval ${reloadInterval / 1000} 6742 // target duration ${details.targetduration} 6743 // distance to edge ${distanceToLiveEdgeMs / 1000}` 6744 // ); 6745 6746 this.timer = self.setTimeout(() => this.loadPlaylist(deliveryDirectives), estimatedTimeUntilUpdate); 6747 } else { 6748 this.clearTimer(); 6749 } 6750 } 6751 getDeliveryDirectives(details, previousDeliveryDirectives, msn, part) { 6752 let skip = getSkipValue(details); 6753 if (previousDeliveryDirectives != null && previousDeliveryDirectives.skip && details.deltaUpdateFailed) { 6754 msn = previousDeliveryDirectives.msn; 6755 part = previousDeliveryDirectives.part; 6756 skip = HlsSkip.No; 6757 } 6758 return new HlsUrlParameters(msn, part, skip); 6759 } 6760 checkRetry(errorEvent) { 6761 const errorDetails = errorEvent.details; 6762 const isTimeout = isTimeoutError(errorEvent); 6763 const errorAction = errorEvent.errorAction; 6764 const { 6765 action, 6766 retryCount = 0, 6767 retryConfig 6768 } = errorAction || {}; 6769 const retry = !!errorAction && !!retryConfig && (action === NetworkErrorAction.RetryRequest || !errorAction.resolved && action === NetworkErrorAction.SendAlternateToPenaltyBox); 6770 if (retry) { 6771 var _errorEvent$context; 6772 this.requestScheduled = -1; 6773 if (retryCount >= retryConfig.maxNumRetry) { 6774 return false; 6775 } 6776 if (isTimeout && (_errorEvent$context = errorEvent.context) != null && _errorEvent$context.deliveryDirectives) { 6777 // The LL-HLS request already timed out so retry immediately 6778 this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" without delivery-directives`); 6779 this.loadPlaylist(); 6780 } else { 6781 const delay = getRetryDelay(retryConfig, retryCount); 6782 // Schedule level/track reload 6783 this.timer = self.setTimeout(() => this.loadPlaylist(), delay); 6784 this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" in ${delay}ms`); 6785 } 6786 // `levelRetry = true` used to inform other controllers that a retry is happening 6787 errorEvent.levelRetry = true; 6788 errorAction.resolved = true; 6789 } 6790 return retry; 6791 } 6792} 6793 6794/* 6795 * compute an Exponential Weighted moving average 6796 * - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 6797 * - heavily inspired from shaka-player 6798 */ 6799 6800class EWMA { 6801 // About half of the estimated value will be from the last |halfLife| samples by weight. 6802 constructor(halfLife, estimate = 0, weight = 0) { 6803 this.halfLife = void 0; 6804 this.alpha_ = void 0; 6805 this.estimate_ = void 0; 6806 this.totalWeight_ = void 0; 6807 this.halfLife = halfLife; 6808 // Larger values of alpha expire historical data more slowly. 6809 this.alpha_ = halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0; 6810 this.estimate_ = estimate; 6811 this.totalWeight_ = weight; 6812 } 6813 sample(weight, value) { 6814 const adjAlpha = Math.pow(this.alpha_, weight); 6815 this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_; 6816 this.totalWeight_ += weight; 6817 }
vendor: 4,303 bytes, lines 6818-6942
6818 getTotalWeight() { 6819 return this.totalWeight_; 6820 } 6821 getEstimate() { 6822 if (this.alpha_) { 6823 const zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_); 6824 if (zeroFactor) { 6825 return this.estimate_ / zeroFactor; 6826 } 6827 } 6828 return this.estimate_; 6829 } 6830} 6831 6832/* 6833 * EWMA Bandwidth Estimator 6834 * - heavily inspired from shaka-player 6835 * Tracks bandwidth samples and estimates available bandwidth. 6836 * Based on the minimum of two exponentially-weighted moving averages with 6837 * different half-lives. 6838 */ 6839 6840class EwmaBandWidthEstimator { 6841 constructor(slow, fast, defaultEstimate, defaultTTFB = 100) { 6842 this.defaultEstimate_ = void 0; 6843 this.minWeight_ = void 0; 6844 this.minDelayMs_ = void 0; 6845 this.slow_ = void 0; 6846 this.fast_ = void 0; 6847 this.defaultTTFB_ = void 0; 6848 this.ttfb_ = void 0; 6849 this.defaultEstimate_ = defaultEstimate; 6850 this.minWeight_ = 0.001; 6851 this.minDelayMs_ = 50; 6852 this.slow_ = new EWMA(slow); 6853 this.fast_ = new EWMA(fast); 6854 this.defaultTTFB_ = defaultTTFB; 6855 this.ttfb_ = new EWMA(slow); 6856 } 6857 update(slow, fast) { 6858 const { 6859 slow_, 6860 fast_, 6861 ttfb_ 6862 } = this; 6863 if (slow_.halfLife !== slow) { 6864 this.slow_ = new EWMA(slow, slow_.getEstimate(), slow_.getTotalWeight()); 6865 } 6866 if (fast_.halfLife !== fast) { 6867 this.fast_ = new EWMA(fast, fast_.getEstimate(), fast_.getTotalWeight()); 6868 } 6869 if (ttfb_.halfLife !== slow) { 6870 this.ttfb_ = new EWMA(slow, ttfb_.getEstimate(), ttfb_.getTotalWeight()); 6871 } 6872 } 6873 sample(durationMs, numBytes) { 6874 durationMs = Math.max(durationMs, this.minDelayMs_); 6875 const numBits = 8 * numBytes; 6876 // weight is duration in seconds 6877 const durationS = durationMs / 1000; 6878 // value is bandwidth in bits/s 6879 const bandwidthInBps = numBits / durationS; 6880 this.fast_.sample(durationS, bandwidthInBps); 6881 this.slow_.sample(durationS, bandwidthInBps); 6882 } 6883 sampleTTFB(ttfb) { 6884 // weight is frequency curve applied to TTFB in seconds 6885 // (longer times have less weight with expected input under 1 second) 6886 const seconds = ttfb / 1000; 6887 const weight = Math.sqrt(2) * Math.exp(-Math.pow(seconds, 2) / 2); 6888 this.ttfb_.sample(weight, Math.max(ttfb, 5)); 6889 } 6890 canEstimate() { 6891 return this.fast_.getTotalWeight() >= this.minWeight_; 6892 } 6893 getEstimate() { 6894 if (this.canEstimate()) { 6895 // console.log('slow estimate:'+ Math.round(this.slow_.getEstimate())); 6896 // console.log('fast estimate:'+ Math.round(this.fast_.getEstimate())); 6897 // Take the minimum of these two estimates. This should have the effect of 6898 // adapting down quickly, but up more slowly. 6899 return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate()); 6900 } else { 6901 return this.defaultEstimate_; 6902 } 6903 } 6904 getEstimateTTFB() { 6905 if (this.ttfb_.getTotalWeight() >= this.minWeight_) { 6906 return this.ttfb_.getEstimate(); 6907 } else { 6908 return this.defaultTTFB_; 6909 } 6910 } 6911 destroy() {} 6912} 6913 6914const SUPPORTED_INFO_DEFAULT = { 6915 supported: true, 6916 configurations: [], 6917 decodingInfoResults: [{ 6918 supported: true, 6919 powerEfficient: true, 6920 smooth: true 6921 }] 6922}; 6923const SUPPORTED_INFO_CACHE = {}; 6924function requiresMediaCapabilitiesDecodingInfo(level, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference) { 6925 // Only test support when configuration is exceeds minimum options 6926 const audioGroups = level.audioCodec ? level.audioGroups : null; 6927 const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; 6928 const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; 6929 const maxChannels = channelsPreference ? parseInt(channelsPreference) : audioCodecPreference ? Infinity : 2; 6930 let audioChannels = null; 6931 if (audioGroups != null && audioGroups.length) { 6932 try { 6933 if (audioGroups.length === 1 && audioGroups[0]) { 6934 audioChannels = audioTracksByGroup.groups[audioGroups[0]].channels; 6935 } else { 6936 audioChannels = audioGroups.reduce((acc, groupId) => { 6937 if (groupId) { 6938 const audioTrackGroup = audioTracksByGroup.groups[groupId]; 6939 if (!audioTrackGroup) { 6940 throw new Error(`Audio track group ${groupId} not found`); 6941 } 6942 // Sum all channel key values
6943 Object.keys(audioTrackGroup.channels).forEach(key => { 6944 acc[key] = (acc[key] || 0) + audioTrackGroup.channels[key]; 6945 }); 6946 } 6947 return acc; 6948 }, { 6949 2: 0 6950 }); 6951 } 6952 } catch (error) { 6953 return true; 6954 } 6955 } 6956 return level.videoCodec !== undefined && (level.width > 1920 && level.height > 1088 || level.height > 1920 && level.width > 1088 || level.frameRate > Math.max(currentFrameRate, 30) || level.videoRange !== 'SDR' && level.videoRange !== currentVideoRange || level.bitrate > Math.max(currentBw, 8e6)) || !!audioChannels && isFiniteNumber(maxChannels) && Object.keys(audioChannels).some(channels => parseInt(channels) > maxChannels); 6957} 6958function getMediaDecodingInfoPromise(level, audioTracksByGroup, mediaCapabilities) { 6959 const videoCodecs = level.videoCodec; 6960 const audioCodecs = level.audioCodec; 6961 if (!videoCodecs || !audioCodecs || !mediaCapabilities) { 6962 return Promise.resolve(SUPPORTED_INFO_DEFAULT); 6963 } 6964 const baseVideoConfiguration = { 6965 width: level.width, 6966 height: level.height, 6967 bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)), 6968 // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0. 6969 framerate: level.frameRate || 30 6970 }; 6971 const videoRange = level.videoRange; 6972 if (videoRange !== 'SDR') { 6973 baseVideoConfiguration.transferFunction = videoRange.toLowerCase(); 6974 } 6975 const configurations = videoCodecs.split(',').map(videoCodec => ({ 6976 type: 'media-source', 6977 video: _objectSpread2(_objectSpread2({}, baseVideoConfiguration), {}, { 6978 contentType: mimeTypeForCodec(videoCodec, 'video') 6979 }) 6980 })); 6981 if (audioCodecs && level.audioGroups) { 6982 level.audioGroups.forEach(audioGroupId => { 6983 var _audioTracksByGroup$g; 6984 if (!audioGroupId) { 6985 return; 6986 } 6987 (_audioTracksByGroup$g = audioTracksByGroup.groups[audioGroupId]) == null ? void 0 : _audioTracksByGroup$g.tracks.forEach(audioTrack => { 6988 if (audioTrack.groupId === audioGroupId) { 6989 const channels = audioTrack.channels || ''; 6990 const channelsNumber = parseFloat(channels); 6991 if (isFiniteNumber(channelsNumber) && channelsNumber > 2) { 6992 configurations.push.apply(configurations, audioCodecs.split(',').map(audioCodec => ({ 6993 type: 'media-source', 6994 audio: { 6995 contentType: mimeTypeForCodec(audioCodec, 'audio'), 6996 channels: '' + channelsNumber 6997 // spatialRendering: 6998 // audioCodec === 'ec-3' && channels.indexOf('JOC'), 6999 } 7000 }))); 7001 } 7002 } 7003 }); 7004 }); 7005 } 7006 return Promise.all(configurations.map(configuration => { 7007 // Cache MediaCapabilities promises 7008 const decodingInfoKey = getMediaDecodingInfoKey(configuration); 7009 return SUPPORTED_INFO_CACHE[decodingInfoKey] || (SUPPORTED_INFO_CACHE[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration)); 7010 })).then(decodingInfoResults => ({ 7011 supported: !decodingInfoResults.some(info => !info.supported), 7012 configurations, 7013 decodingInfoResults 7014 })).catch(error => ({ 7015 supported: false, 7016 configurations, 7017 decodingInfoResults: [], 7018 error 7019 })); 7020} 7021function getMediaDecodingInfoKey(config) { 7022 const { 7023 audio, 7024 video 7025 } = config; 7026 const mediaConfig = video || audio; 7027 if (mediaConfig) { 7028 const codec = mediaConfig.contentType.split('"')[1]; 7029 if (video) { 7030 return `r${video.height}x${video.width}f${Math.ceil(video.framerate)}${video.transferFunction || 'sd'}_${codec}_${Math.ceil(video.bitrate / 1e5)}`; 7031 } 7032 if (audio) { 7033 return `c${audio.channels}${audio.spatialRendering ? 's' : 'n'}_${codec}`; 7034 } 7035 } 7036 return ''; 7037} 7038 7039/** 7040 * @returns Whether we can detect and validate HDR capability within the window context 7041 */ 7042function isHdrSupported() { 7043 if (typeof matchMedia === 'function') { 7044 const mediaQueryList = matchMedia('(dynamic-range: high)'); 7045 const badQuery = matchMedia('bad query'); 7046 if (mediaQueryList.media !== badQuery.media) { 7047 return mediaQueryList.matches === true; 7048 } 7049 } 7050 return false;
7051} 7052 7053/** 7054 * Sanitizes inputs to return the active video selection options for HDR/SDR. 7055 * When both inputs are null: 7056 * 7057 * `{ preferHDR: false, allowedVideoRanges: [] }` 7058 * 7059 * When `currentVideoRange` non-null, maintain the active range: 7060 * 7061 * `{ preferHDR: currentVideoRange !== 'SDR', allowedVideoRanges: [currentVideoRange] }` 7062 * 7063 * When VideoSelectionOption non-null: 7064 * 7065 * - Allow all video ranges if `allowedVideoRanges` unspecified. 7066 * - If `preferHDR` is non-null use the value to filter `allowedVideoRanges`. 7067 * - Else check window for HDR support and set `preferHDR` to the result. 7068 * 7069 * @param currentVideoRange 7070 * @param videoPreference 7071 */ 7072function getVideoSelectionOptions(currentVideoRange, videoPreference) { 7073 let preferHDR = false; 7074 let allowedVideoRanges = []; 7075 if (currentVideoRange) { 7076 preferHDR = currentVideoRange !== 'SDR'; 7077 allowedVideoRanges = [currentVideoRange]; 7078 } 7079 if (videoPreference) { 7080 allowedVideoRanges = videoPreference.allowedVideoRanges || VideoRangeValues.slice(0); 7081 preferHDR = videoPreference.preferHDR !== undefined ? videoPreference.preferHDR : isHdrSupported(); 7082 if (preferHDR) { 7083 allowedVideoRanges = allowedVideoRanges.filter(range => range !== 'SDR'); 7084 } else { 7085 allowedVideoRanges = ['SDR']; 7086 } 7087 } 7088 return { 7089 preferHDR, 7090 allowedVideoRanges 7091 }; 7092} 7093 7094function getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference) { 7095 const codecSets = Object.keys(codecTiers); 7096 const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; 7097 const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; 7098 const preferStereo = channelsPreference && parseInt(channelsPreference) === 2; 7099 // Use first level set to determine stereo, and minimum resolution and framerate 7100 let hasStereo = true; 7101 let hasCurrentVideoRange = false; 7102 let minHeight = Infinity; 7103 let minFramerate = Infinity; 7104 let minBitrate = Infinity; 7105 let selectedScore = 0; 7106 let videoRanges = []; 7107 const { 7108 preferHDR, 7109 allowedVideoRanges 7110 } = getVideoSelectionOptions(currentVideoRange, videoPreference); 7111 for (let i = codecSets.length; i--;) { 7112 const tier = codecTiers[codecSets[i]]; 7113 hasStereo = tier.channels[2] > 0; 7114 minHeight = Math.min(minHeight, tier.minHeight); 7115 minFramerate = Math.min(minFramerate, tier.minFramerate); 7116 minBitrate = Math.min(minBitrate, tier.minBitrate); 7117 const matchingVideoRanges = allowedVideoRanges.filter(range => tier.videoRanges[range] > 0); 7118 if (matchingVideoRanges.length > 0) { 7119 hasCurrentVideoRange = true; 7120 videoRanges = matchingVideoRanges; 7121 } 7122 } 7123 minHeight = isFiniteNumber(minHeight) ? minHeight : 0; 7124 minFramerate = isFiniteNumber(minFramerate) ? minFramerate : 0; 7125 const maxHeight = Math.max(1080, minHeight); 7126 const maxFramerate = Math.max(30, minFramerate); 7127 minBitrate = isFiniteNumber(minBitrate) ? minBitrate : currentBw; 7128 currentBw = Math.max(minBitrate, currentBw); 7129 // If there are no variants with matching preference, set currentVideoRange to undefined 7130 if (!hasCurrentVideoRange) { 7131 currentVideoRange = undefined; 7132 videoRanges = []; 7133 } 7134 const codecSet = codecSets.reduce((selected, candidate) => { 7135 // Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present 7136 const candidateTier = codecTiers[candidate]; 7137 if (candidate === selected) { 7138 return selected; 7139 } 7140 if (candidateTier.minBitrate > currentBw) { 7141 logStartCodecCandidateIgnored(candidate, `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`); 7142 return selected; 7143 } 7144 if (!candidateTier.hasDefaultAudio) { 7145 logStartCodecCandidateIgnored(candidate, `no renditions with default or auto-select sound found`); 7146 return selected; 7147 } 7148 if (audioCodecPreference && candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0) { 7149 logStartCodecCandidateIgnored(candidate, `audio codec preference "${audioCodecPreference}" not found`); 7150 return selected; 7151 } 7152 if (channelsPreference && !preferStereo) { 7153 if (!candidateTier.channels[channelsPreference]) { 7154 logStartCodecCandidateIgnored(candidate, `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(candidateTier.channels)})`); 7155 return selected; 7156 } 7157 } else if ((!audioCodecPreference || preferStereo) && hasStereo && candidateTier.channels['2'] === 0) { 7158 logStartCodecCandidateIgnored(candidate, `no renditions with stereo sound found`); 7159 return selected; 7160 } 7161 if (candidateTier.minHeight > maxHeight) { 7162 logStartCodecCandidateIgnored(candidate, `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`); 7163 return selected; 7164 } 7165 if (candidateTier.minFramerate > maxFramerate) { 7166 logStartCodecCandidateIgnored(candidate, `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`); 7167 return selected; 7168 } 7169 if (!videoRanges.some(range => candidateTier.videoRanges[range] > 0)) { 7170 logStartCodecCandidateIgnored(candidate, `no variants with VIDEO-RANGE of ${JSON.stringify(videoRanges)} found`); 7171 return selected; 7172 } 7173 if (candidateTier.maxScore < selectedScore) { 7174 logStartCodecCandidateIgnored(candidate, `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`); 7175 return selected; 7176 } 7177 // Remove candiates with less preferred codecs or more errors 7178 if (selected && (codecsSetSelectionPreferenceValue(candidate) >
vendor: 4,457 bytes, lines 7178-7301
7178= codecsSetSelectionPreferenceValue(selected) || candidateTier.fragmentError > codecTiers[selected].fragmentError)) { 7179 return selected; 7180 } 7181 selectedScore = candidateTier.maxScore; 7182 return candidate; 7183 }, undefined); 7184 return { 7185 codecSet, 7186 videoRanges, 7187 preferHDR, 7188 minFramerate, 7189 minBitrate 7190 }; 7191} 7192function logStartCodecCandidateIgnored(codeSet, reason) { 7193 logger.log(`[abr] start candidates with "${codeSet}" ignored because ${reason}`); 7194} 7195function getAudioTracksByGroup(allAudioTracks) { 7196 return allAudioTracks.reduce((audioTracksByGroup, track) => { 7197 let trackGroup = audioTracksByGroup.groups[track.groupId]; 7198 if (!trackGroup) { 7199 trackGroup = audioTracksByGroup.groups[track.groupId] = { 7200 tracks: [], 7201 channels: { 7202 2: 0 7203 }, 7204 hasDefault: false, 7205 hasAutoSelect: false 7206 }; 7207 } 7208 trackGroup.tracks.push(track); 7209 const channelsKey = track.channels || '2'; 7210 trackGroup.channels[channelsKey] = (trackGroup.channels[channelsKey] || 0) + 1; 7211 trackGroup.hasDefault = trackGroup.hasDefault || track.default; 7212 trackGroup.hasAutoSelect = trackGroup.hasAutoSelect || track.autoselect; 7213 if (trackGroup.hasDefault) { 7214 audioTracksByGroup.hasDefaultAudio = true; 7215 } 7216 if (trackGroup.hasAutoSelect) { 7217 audioTracksByGroup.hasAutoSelectAudio = true; 7218 } 7219 return audioTracksByGroup; 7220 }, { 7221 hasDefaultAudio: false, 7222 hasAutoSelectAudio: false, 7223 groups: {} 7224 }); 7225} 7226function getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel) { 7227 return levels.slice(minAutoLevel, maxAutoLevel + 1).reduce((tiers, level) => { 7228 if (!level.codecSet) { 7229 return tiers; 7230 } 7231 const audioGroups = level.audioGroups; 7232 let tier = tiers[level.codecSet]; 7233 if (!tier) { 7234 tiers[level.codecSet] = tier = { 7235 minBitrate: Infinity, 7236 minHeight: Infinity, 7237 minFramerate: Infinity, 7238 maxScore: 0, 7239 videoRanges: { 7240 SDR: 0 7241 }, 7242 channels: { 7243 '2': 0 7244 }, 7245 hasDefaultAudio: !audioGroups, 7246 fragmentError: 0 7247 }; 7248 } 7249 tier.minBitrate = Math.min(tier.minBitrate, level.bitrate); 7250 const lesserWidthOrHeight = Math.min(level.height, level.width); 7251 tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight); 7252 tier.minFramerate = Math.min(tier.minFramerate, level.frameRate); 7253 tier.maxScore = Math.max(tier.maxScore, level.score); 7254 tier.fragmentError += level.fragmentError; 7255 tier.videoRanges[level.videoRange] = (tier.videoRanges[level.videoRange] || 0) + 1; 7256 if (audioGroups) { 7257 audioGroups.forEach(audioGroupId => { 7258 if (!audioGroupId) { 7259 return; 7260 } 7261 const audioGroup = audioTracksByGroup.groups[audioGroupId]; 7262 if (!audioGroup) { 7263 return; 7264 } 7265 // Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants 7266 tier.hasDefaultAudio = tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio ? audioGroup.hasDefault : audioGroup.hasAutoSelect || !audioTracksByGroup.hasDefaultAudio && !audioTracksByGroup.hasAutoSelectAudio; 7267 Object.keys(audioGroup.channels).forEach(channels => { 7268 tier.channels[channels] = (tier.channels[channels] || 0) + audioGroup.channels[channels]; 7269 }); 7270 }); 7271 } 7272 return tiers; 7273 }, {}); 7274} 7275function findMatchingOption(option, tracks, matchPredicate) { 7276 if ('attrs' in option) { 7277 const index = tracks.indexOf(option); 7278 if (index !== -1) { 7279 return index; 7280 } 7281 } 7282 for (let i = 0; i < tracks.length; i++) { 7283 const track = tracks[i]; 7284 if (matchesOption(option, track, matchPredicate)) { 7285 return i; 7286 } 7287 } 7288 return -1; 7289} 7290function matchesOption(option, track, matchPredicate) { 7291 const { 7292 groupId, 7293 name, 7294 lang, 7295 assocLang, 7296 characteristics, 7297 default: isDefault 7298 } = option; 7299 const forced = option.forced; 7300 return (groupId === undefined || track.groupId === groupId) && (name === undefined || track.name === name) && (lang === undefined || track.lang === lang) && (lang === undefined || track.assocLang === assocLang) && (isDefault === undefined || track.default === isDefault) && (forced === undefined || track.forced === forced) && (characteristics === undefined || characteristicsMatch(characteristics, track.characteristics)) && (matchPredicate === undefined || matchPredicate(option, track)); 7301}
7302function characteristicsMatch(characteristicsA, characteristicsB = '') { 7303 const arrA = characteristicsA.split(','); 7304 const arrB = characteristicsB.split(','); 7305 // Expects each item to be unique: 7306 return arrA.length === arrB.length && !arrA.some(el => arrB.indexOf(el) === -1); 7307} 7308function audioMatchPredicate(option, track) { 7309 const { 7310 audioCodec, 7311 channels 7312 } = option; 7313 return (audioCodec === undefined || (track.audioCodec || '').substring(0, 4) === audioCodec.substring(0, 4)) && (channels === undefined || channels === (track.channels || '2')); 7314} 7315function findClosestLevelWithAudioGroup(option, levels, allAudioTracks, searchIndex, matchPredicate) { 7316 const currentLevel = levels[searchIndex]; 7317 // Are there variants with same URI as current level? 7318 // If so, find a match that does not require any level URI change 7319 const variants = levels.reduce((variantMap, level, index) => { 7320 const uri = level.uri; 7321 const renditions = variantMap[uri] || (variantMap[uri] = []); 7322 renditions.push(index); 7323 return variantMap; 7324 }, {}); 7325 const renditions = variants[currentLevel.uri]; 7326 if (renditions.length > 1) { 7327 searchIndex = Math.max.apply(Math, renditions); 7328 } 7329 // Find best match 7330 const currentVideoRange = currentLevel.videoRange; 7331 const currentFrameRate = currentLevel.frameRate; 7332 const currentVideoCodec = currentLevel.codecSet.substring(0, 4); 7333 const matchingVideo = searchDownAndUpList(levels, searchIndex, level => { 7334 if (level.videoRange !== currentVideoRange || level.frameRate !== currentFrameRate || level.codecSet.substring(0, 4) !== currentVideoCodec) { 7335 return false; 7336 } 7337 const audioGroups = level.audioGroups; 7338 const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 7339 return findMatchingOption(option, tracks, matchPredicate) > -1; 7340 }); 7341 if (matchingVideo > -1) { 7342 return matchingVideo; 7343 } 7344 return searchDownAndUpList(levels, searchIndex, level => { 7345 const audioGroups = level.audioGroups; 7346 const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 7347 return findMatchingOption(option, tracks, matchPredicate) > -1; 7348 }); 7349} 7350function searchDownAndUpList(arr, searchIndex, predicate) { 7351 for (let i = searchIndex; i; i--) { 7352 if (predicate(arr[i])) { 7353 return i; 7354 } 7355 } 7356 for (let i = searchIndex + 1; i < arr.length; i++) { 7357 if (predicate(arr[i])) { 7358 return i; 7359 } 7360 } 7361 return -1; 7362} 7363 7364class AbrController { 7365 constructor(_hls) { 7366 this.hls = void 0; 7367 this.lastLevelLoadSec = 0; 7368 this.lastLoadedFragLevel = -1; 7369 this.firstSelection = -1; 7370 this._nextAutoLevel = -1; 7371 this.nextAutoLevelKey = ''; 7372 this.audioTracksByGroup = null; 7373 this.codecTiers = null; 7374 this.timer = -1; 7375 this.fragCurrent = null; 7376 this.partCurrent = null; 7377 this.bitrateTestDelay = 0; 7378 this.bwEstimator = void 0; 7379 /* 7380 This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load 7381 quickly enough to prevent underbuffering 7382 */ 7383 this._abandonRulesCheck = () => { 7384 const { 7385 fragCurrent: frag, 7386 partCurrent: part, 7387 hls 7388 } = this; 7389 const { 7390 autoLevelEnabled, 7391 media 7392 } = hls; 7393 if (!frag || !media) { 7394 return; 7395 } 7396 const now = performance.now(); 7397 const stats = part ? part.stats : frag.stats; 7398 const duration = part ? part.duration : frag.duration; 7399 const timeLoading = now - stats.loading.start; 7400 const minAutoLevel = hls.minAutoLevel; 7401 // If frag loading is aborted, complete, or from lowest level, stop timer and return 7402 if (stats.aborted || stats.loaded && stats.loaded === stats.total || frag.level <= minAutoLevel) { 7403 this.clearTimer(); 7404 // reset forced auto level value so that next level will be selected 7405 this._nextAutoLevel = -1; 7406 return; 7407 } 7408 7409 // This check only runs if we're in ABR mode and actually playing 7410 if (!autoLevelEnabled || media.paused || !media.playbackRate || !media.readyState) { 7411 return; 7412 } 7413 const bufferInfo = hls.mainForwardBufferInfo; 7414 if (bufferInfo === null) { 7415 return; 7416 } 7417 const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); 7418 const playbackRate = Math.abs(media.playbackRate); 7419 // To maintain stable adaptive playback, only begin monitoring frag loading after half or more of its playback duration has passed 7420 if (timeLoading <= Math.max(ttfbEstimate, 1000 * (duration / (playbackRate * 2)))) { 7421 return; 7422 } 7423 7424 // bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer 7425 const bufferStarvationDelay = bufferInfo.len / playbackRate; 7426 const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; 7427 const loadedFirstByte = stats.loaded && ttfb > -1; 7428 const bwEstimate = this.getBwEstimate(); 7429 const levels = hls.levels; 7430 const level = levels[frag.level]; 7431 const expectedLen = stats.total || Math.max(stats.loaded, Math.round(duration * level.averageBitrate / 8)); 7432 let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading; 7433 if (timeStreaming < 1 && loadedFirstByte) { 7434 timeStreaming = Math.min(timeLoading, stats.loaded * 8 / bwEstimate); 7435 } 7436 const loadRate = loadedFirstByte ? stats.loaded * 1000 / timeStreaming : 0; 7437 // fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment 7438 const fragLoadedDelay = loadRate ? (expectedLen - stats.loaded) / loadRate : expectedLen * 8 / bwEstimate + ttfbEstimate / 1000; 7439 // Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left 7440 if (fragLoadedDelay <= bufferStarvationDelay) { 7441 return; 7442 } 7443 const bwe = loadRate ? loadRate * 8 : bwEstimate; 7444 let fragLevelNextLoadedDelay = Number.POSITIVE_INFINITY; 7445 let nextLoadLevel; 7446 // Iterate through lower level and try to find the largest one that avoids rebuffering 7447 for (nextLoadLevel = frag.level - 1; nextLoadLevel > minAutoLevel; nextLoadLevel--) { 7448 // compute time to load next fragment at lower level 7449 // 8 = bits per byte (bps/Bps) 7450 const levelNextBitrate = levels[nextLoadLevel].maxBitrate; 7451 fragLevelNextLoadedDelay = this.getTimeToLoadFrag(ttfbEstimate / 1000, bwe, duration * levelNextBitrate, !levels[nextLoadLevel].details); 7452 if (fragLevelNextLoadedDelay < bufferStarvationDelay) { 7453 break; 7454 } 7455 } 7456 // Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing 7457 // to load the current one 7458 if (fragLevelNextLoadedDelay >= fragLoadedDelay) { 7459 return; 7460 } 7461 7462 // if estimated load time of new segment is completely unreasonable, ignore and do not emergency switch down 7463 if (fragLevelNextLoadedDelay > duration * 10) { 7464 return; 7465 } 7466 hls.nextLoadLevel = hls.nextAutoLevel = nextLoadLevel; 7467 if (loadedFirstByte) { 7468 // If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time 7469 this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); 7470 } else { 7471 // If there has been no loading progress, sample TTFB 7472 this.bwEstimator.sampleTTFB(timeLoading); 7473 } 7474 const nextLoadLevelBitrate = levels[nextLoadLevel].maxBitrate; 7475 if (this.getBwEstimate() * this.hls.config.abrBandWidthUpFactor >
vendor: 7,307 bytes, lines 7475-7678
7475 nextLoadLevelBitrate) { 7476 this.resetEstimator(nextLoadLevelBitrate); 7477 } 7478 this.clearTimer(); 7479 logger.warn(`[abr] Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} is loading too slowly; 7480 Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s 7481 Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s 7482 Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(3)} s 7483 TTFB estimate: ${ttfb | 0} ms 7484 Current BW estimate: ${isFiniteNumber(bwEstimate) ? bwEstimate | 0 : 'Unknown'} bps 7485 New BW estimate: ${this.getBwEstimate() | 0} bps 7486 Switching to level ${nextLoadLevel} @ ${nextLoadLevelBitrate | 0} bps`); 7487 hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { 7488 frag, 7489 part, 7490 stats 7491 }); 7492 }; 7493 this.hls = _hls; 7494 this.bwEstimator = this.initEstimator(); 7495 this.registerListeners(); 7496 } 7497 resetEstimator(abrEwmaDefaultEstimate) { 7498 if (abrEwmaDefaultEstimate) { 7499 logger.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`); 7500 this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate; 7501 } 7502 this.firstSelection = -1; 7503 this.bwEstimator = this.initEstimator(); 7504 } 7505 initEstimator() { 7506 const config = this.hls.config; 7507 return new EwmaBandWidthEstimator(config.abrEwmaSlowVoD, config.abrEwmaFastVoD, config.abrEwmaDefaultEstimate); 7508 } 7509 registerListeners() { 7510 const { 7511 hls 7512 } = this; 7513 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 7514 hls.on(Events.FRAG_LOADING, this.onFragLoading, this); 7515 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 7516 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 7517 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 7518 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 7519 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 7520 hls.on(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); 7521 hls.on(Events.ERROR, this.onError, this); 7522 } 7523 unregisterListeners() { 7524 const { 7525 hls 7526 } = this; 7527 if (!hls) { 7528 return; 7529 } 7530 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 7531 hls.off(Events.FRAG_LOADING, this.onFragLoading, this); 7532 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 7533 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 7534 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 7535 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 7536 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 7537 hls.off(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); 7538 hls.off(Events.ERROR, this.onError, this); 7539 } 7540 destroy() { 7541 this.unregisterListeners(); 7542 this.clearTimer(); 7543 // @ts-ignore 7544 this.hls = this._abandonRulesCheck = null; 7545 this.fragCurrent = this.partCurrent = null; 7546 } 7547 onManifestLoading(event, data) { 7548 this.lastLoadedFragLevel = -1; 7549 this.firstSelection = -1; 7550 this.lastLevelLoadSec = 0; 7551 this.fragCurrent = this.partCurrent = null; 7552 this.onLevelsUpdated(); 7553 this.clearTimer(); 7554 } 7555 onLevelsUpdated() { 7556 if (this.lastLoadedFragLevel > -1 && this.fragCurrent) { 7557 this.lastLoadedFragLevel = this.fragCurrent.level; 7558 } 7559 this._nextAutoLevel = -1; 7560 this.onMaxAutoLevelUpdated(); 7561 this.codecTiers = null; 7562 this.audioTracksByGroup = null; 7563 } 7564 onMaxAutoLevelUpdated() { 7565 this.firstSelection = -1; 7566 this.nextAutoLevelKey = ''; 7567 } 7568 onFragLoading(event, data) { 7569 const frag = data.frag; 7570 if (this.ignoreFragment(frag)) { 7571 return; 7572 } 7573 if (!frag.bitrateTest) { 7574 var _data$part; 7575 this.fragCurrent = frag; 7576 this.partCurrent = (_data$part = data.part) != null ? _data$part : null; 7577 } 7578 this.clearTimer(); 7579 this.timer = self.setInterval(this._abandonRulesCheck, 100); 7580 } 7581 onLevelSwitching(event, data) { 7582 this.clearTimer(); 7583 } 7584 onError(event, data) { 7585 if (data.fatal) { 7586 return; 7587 } 7588 switch (data.details) { 7589 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 7590 case ErrorDetails.BUFFER_APPEND_ERROR: 7591 // Reset last loaded level so that a new selection can be made after calling recoverMediaError 7592 this.lastLoadedFragLevel = -1; 7593 this.firstSelection = -1; 7594 break; 7595 case ErrorDetails.FRAG_LOAD_TIMEOUT: 7596 { 7597 const frag = data.frag; 7598 const { 7599 fragCurrent, 7600 partCurrent: part 7601 } = this; 7602 if (frag && fragCurrent && frag.sn === fragCurrent.sn && frag.level === fragCurrent.level) { 7603 const now = performance.now(); 7604 const stats = part ? part.stats : frag.stats; 7605 const timeLoading = now - stats.loading.start; 7606 const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; 7607 const loadedFirstByte = stats.loaded && ttfb > -1; 7608 if (loadedFirstByte) { 7609 const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); 7610 this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); 7611 } else { 7612 this.bwEstimator.sampleTTFB(timeLoading); 7613 } 7614 } 7615 break; 7616 } 7617 } 7618 } 7619 getTimeToLoadFrag(timeToFirstByteSec, bandwidth, fragSizeBits, isSwitch) { 7620 const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth; 7621 const playlistLoadSec = isSwitch ? this.lastLevelLoadSec : 0; 7622 return fragLoadSec + playlistLoadSec; 7623 } 7624 onLevelLoaded(event, data) { 7625 const config = this.hls.config; 7626 const { 7627 loading 7628 } = data.stats; 7629 const timeLoadingMs = loading.end - loading.start; 7630 if (isFiniteNumber(timeLoadingMs)) { 7631 this.lastLevelLoadSec = timeLoadingMs / 1000; 7632 } 7633 if (data.details.live) { 7634 this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive); 7635 } else { 7636 this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD); 7637 } 7638 } 7639 onFragLoaded(event, { 7640 frag, 7641 part 7642 }) { 7643 const stats = part ? part.stats : frag.stats; 7644 if (frag.type === PlaylistLevelType.MAIN) { 7645 this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start); 7646 } 7647 if (this.ignoreFragment(frag)) { 7648 return; 7649 } 7650 // stop monitoring bw once frag loaded 7651 this.clearTimer(); 7652 // reset forced auto level value so that next level will be selected 7653 if (frag.level === this._nextAutoLevel) { 7654 this._nextAutoLevel = -1; 7655 } 7656 this.firstSelection = -1; 7657 7658 // compute level average bitrate 7659 if (this.hls.config.abrMaxWithRealBitrate) { 7660 const duration = part ? part.duration : frag.duration; 7661 const level = this.hls.levels[frag.level]; 7662 const loadedBytes = (level.loaded ? level.loaded.bytes : 0) + stats.loaded; 7663 const loadedDuration = (level.loaded ? level.loaded.duration : 0) + duration; 7664 level.loaded = { 7665 bytes: loadedBytes, 7666 duration: loadedDuration 7667 }; 7668 level.realBitrate = Math.round(8 * loadedBytes / loadedDuration); 7669 } 7670 if (frag.bitrateTest) { 7671 const fragBufferedData = { 7672 stats, 7673 frag, 7674 part, 7675 id: frag.type 7676 }; 7677 this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData); 7678 frag.bitrateTest = false;
vendor: 3,466 bytes, lines 7679-7769
7679 } else { 7680 // store level id after successful fragment load for playback 7681 this.lastLoadedFragLevel = frag.level; 7682 } 7683 } 7684 onFragBuffered(event, data) { 7685 const { 7686 frag, 7687 part 7688 } = data; 7689 const stats = part != null && part.stats.loaded ? part.stats : frag.stats; 7690 if (stats.aborted) { 7691 return; 7692 } 7693 if (this.ignoreFragment(frag)) { 7694 return; 7695 } 7696 // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing; 7697 // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch 7698 // is used. If we used buffering in that case, our BW estimate sample will be very large. 7699 const processingMs = stats.parsing.end - stats.loading.start - Math.min(stats.loading.first - stats.loading.start, this.bwEstimator.getEstimateTTFB()); 7700 this.bwEstimator.sample(processingMs, stats.loaded); 7701 stats.bwEstimate = this.getBwEstimate(); 7702 if (frag.bitrateTest) { 7703 this.bitrateTestDelay = processingMs / 1000; 7704 } else { 7705 this.bitrateTestDelay = 0; 7706 } 7707 } 7708 ignoreFragment(frag) { 7709 // Only count non-alt-audio frags which were actually buffered in our BW calculations 7710 return frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment'; 7711 } 7712 clearTimer() { 7713 if (this.timer > -1) { 7714 self.clearInterval(this.timer); 7715 this.timer = -1; 7716 } 7717 } 7718 get firstAutoLevel() { 7719 const { 7720 maxAutoLevel, 7721 minAutoLevel 7722 } = this.hls; 7723 const bwEstimate = this.getBwEstimate(); 7724 const maxStartDelay = this.hls.config.maxStarvationDelay; 7725 const abrAutoLevel = this.findBestLevel(bwEstimate, minAutoLevel, maxAutoLevel, 0, maxStartDelay, 1, 1); 7726 if (abrAutoLevel > -1) { 7727 return abrAutoLevel; 7728 } 7729 const firstLevel = this.hls.firstLevel; 7730 const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel); 7731 logger.warn(`[abr] Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`); 7732 return clamped; 7733 } 7734 get forcedAutoLevel() { 7735 if (this.nextAutoLevelKey) { 7736 return -1; 7737 } 7738 return this._nextAutoLevel; 7739 } 7740 7741 // return next auto level 7742 get nextAutoLevel() { 7743 const forcedAutoLevel = this.forcedAutoLevel; 7744 const bwEstimator = this.bwEstimator; 7745 const useEstimate = bwEstimator.canEstimate(); 7746 const loadedFirstFrag = this.lastLoadedFragLevel > -1; 7747 // in case next auto level has been forced, and bw not available or not reliable, return forced value 7748 if (forcedAutoLevel !== -1 && (!useEstimate || !loadedFirstFrag || this.nextAutoLevelKey === this.getAutoLevelKey())) { 7749 return forcedAutoLevel; 7750 } 7751 7752 // compute next level using ABR logic 7753 const nextABRAutoLevel = useEstimate && loadedFirstFrag ? this.getNextABRAutoLevel() : this.firstAutoLevel; 7754 7755 // use forced auto level while it hasn't errored more than ABR selection 7756 if (forcedAutoLevel !== -1) { 7757 const levels = this.hls.levels; 7758 if (levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) && levels[forcedAutoLevel].loadError <= levels[nextABRAutoLevel].loadError) { 7759 return forcedAutoLevel; 7760 } 7761 } 7762 7763 // save result until state has changed 7764 this._nextAutoLevel = nextABRAutoLevel; 7765 this.nextAutoLevelKey = this.getAutoLevelKey(); 7766 return nextABRAutoLevel; 7767 } 7768 getAutoLevelKey() { 7769 return `${this.getBwEstimate()}
7769_${this.getStarvationDelay().toFixed(2)}`; 7770 } 7771 getNextABRAutoLevel() { 7772 const { 7773 fragCurrent, 7774 partCurrent, 7775 hls 7776 } = this; 7777 const { 7778 maxAutoLevel, 7779 config, 7780 minAutoLevel 7781 } = hls; 7782 const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; 7783 const avgbw = this.getBwEstimate(); 7784 // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted. 7785 const bufferStarvationDelay = this.getStarvationDelay(); 7786 let bwFactor = config.abrBandWidthFactor; 7787 let bwUpFactor = config.abrBandWidthUpFactor; 7788 7789 // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all 7790 if (bufferStarvationDelay) { 7791 const _bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, 0, bwFactor, bwUpFactor); 7792 if (_bestLevel >= 0) { 7793 return _bestLevel; 7794 } 7795 } 7796 // not possible to get rid of rebuffering... try to find level that will guarantee less than maxStarvationDelay of rebuffering 7797 let maxStarvationDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxStarvationDelay) : config.maxStarvationDelay; 7798 if (!bufferStarvationDelay) { 7799 // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test 7800 const bitrateTestDelay = this.bitrateTestDelay; 7801 if (bitrateTestDelay) { 7802 // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value 7803 // max video loading delay used in automatic start level selection : 7804 // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level + 7805 // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` ) 7806 // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration 7807 const maxLoadingDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxLoadingDelay) : config.maxLoadingDelay; 7808 maxStarvationDelay = maxLoadingDelay - bitrateTestDelay; 7809 logger.info(`[abr] bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`); 7810 // don't use conservative factor on bitrate test 7811 bwFactor = bwUpFactor = 1; 7812 } 7813 } 7814 const bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor); 7815 logger.info(`[abr] ${bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'}, optimal quality level ${bestLevel}`); 7816 if (bestLevel > -1) { 7817 return bestLevel; 7818 } 7819 // If no matching level found, see if min auto level would be a better option 7820 const minLevel = hls.levels[minAutoLevel]; 7821 const autoLevel = hls.levels[hls.loadLevel]; 7822 if ((minLevel == null ? void 0 : minLevel.bitrate) < (autoLevel == null ? void 0 : autoLevel.bitrate)) { 7823 return minAutoLevel; 7824 } 7825 // or if bitrate is not lower, continue to use loadLevel 7826 return hls.loadLevel; 7827 } 7828 getStarvationDelay() { 7829 const hls = this.hls; 7830 const media = hls.media; 7831 if (!media) { 7832 return Infinity; 7833 } 7834 // playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as 7835 // if we're playing back at the normal rate. 7836 const playbackRate = media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0; 7837 const bufferInfo = hls.mainForwardBufferInfo; 7838 return (bufferInfo ? bufferInfo.len : 0) / playbackRate; 7839 } 7840 getBwEstimate() { 7841 return this.bwEstimator.canEstimate() ? this.bwEstimator.getEstimate() : this.hls.config.abrEwmaDefaultEstimate; 7842 } 7843 findBestLevel(currentBw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor) { 7844 var _level$details; 7845 const maxFetchDuration = bufferStarvationDelay + maxStarvationDelay; 7846 const lastLoadedFragLevel = this.lastLoadedFragLevel; 7847 const selectionBaseLevel = lastLoadedFragLevel === -1 ? this.hls.firstLevel : lastLoadedFragLevel; 7848 const { 7849 fragCurrent, 7850 partCurrent 7851 } = this; 7852 const { 7853 levels, 7854 allAudioTracks, 7855 loadLevel, 7856 config 7857 } = this.hls; 7858 if (levels.length === 1) { 7859 return 0; 7860 } 7861 const level = levels[selectionBaseLevel]; 7862 const live = !!(level != null && (_level$details = level.details) != null && _level$details.live); 7863 const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1;
7864 let currentCodecSet; 7865 let currentVideoRange = 'SDR'; 7866 let currentFrameRate = (level == null ? void 0 : level.frameRate) || 0; 7867 const { 7868 audioPreference, 7869 videoPreference 7870 } = config; 7871 const audioTracksByGroup = this.audioTracksByGroup || (this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks)); 7872 if (firstSelection) { 7873 if (this.firstSelection !== -1) { 7874 return this.firstSelection; 7875 } 7876 const codecTiers = this.codecTiers || (this.codecTiers = getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel)); 7877 const startTier = getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference); 7878 const { 7879 codecSet, 7880 videoRanges, 7881 minFramerate, 7882 minBitrate, 7883 preferHDR 7884 } = startTier; 7885 currentCodecSet = codecSet; 7886 currentVideoRange = preferHDR ? videoRanges[videoRanges.length - 1] : videoRanges[0]; 7887 currentFrameRate = minFramerate; 7888 currentBw = Math.max(currentBw, minBitrate); 7889 logger.log(`[abr] picked start tier ${JSON.stringify(startTier)}`); 7890 } else { 7891 currentCodecSet = level == null ? void 0 : level.codecSet; 7892 currentVideoRange = level == null ? void 0 : level.videoRange; 7893 } 7894 const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; 7895 const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000; 7896 const levelsSkipped = []; 7897 for (let i = maxAutoLevel; i >= minAutoLevel; i--) { 7898 var _levelInfo$supportedR; 7899 const levelInfo = levels[i]; 7900 const upSwitch = i > selectionBaseLevel; 7901 if (!levelInfo) { 7902 continue; 7903 } 7904 if (config.useMediaCapabilities && !levelInfo.supportedResult && !levelInfo.supportedPromise) { 7905 const mediaCapabilities = navigator.mediaCapabilities; 7906 if (typeof (mediaCapabilities == null ? void 0 : mediaCapabilities.decodingInfo) === 'function' && requiresMediaCapabilitiesDecodingInfo(levelInfo, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference)) { 7907 levelInfo.supportedPromise = getMediaDecodingInfoPromise(levelInfo, audioTracksByGroup, mediaCapabilities); 7908 levelInfo.supportedPromise.then(decodingInfo => { 7909 if (!this.hls) { 7910 return; 7911 } 7912 levelInfo.supportedResult = decodingInfo; 7913 const levels = this.hls.levels; 7914 const index = levels.indexOf(levelInfo); 7915 if (decodingInfo.error) { 7916 logger.warn(`[abr] MediaCapabilities decodingInfo error: "${decodingInfo.error}" for level ${index} ${JSON.stringify(decodingInfo)}`); 7917 } else if (!decodingInfo.supported) { 7918 logger.warn(`[abr] Unsupported MediaCapabilities decodingInfo result for level ${index} ${JSON.stringify(decodingInfo)}`); 7919 if (index > -1 && levels.length > 1) { 7920 logger.log(`[abr] Removing unsupported level ${index}`); 7921 this.hls.removeLevel(index); 7922 } 7923 } 7924 }); 7925 } else { 7926 levelInfo.supportedResult = SUPPORTED_INFO_DEFAULT; 7927 } 7928 } 7929 7930 // skip candidates which change codec-family or video-range, 7931 // and which decrease or increase frame-rate for up and down-switch respectfully 7932 if (currentCodecSet && levelInfo.codecSet !== currentCodecSet || currentVideoRange && levelInfo.videoRange !== currentVideoRange || upSwitch && currentFrameRate > levelInfo.frameRate || !upSwitch && currentFrameRate > 0 && currentFrameRate < levelInfo.frameRate || levelInfo.supportedResult && !((_levelInfo$supportedR = levelInfo.supportedResult.decodingInfoResults) != null && _levelInfo$supportedR[0].smooth)) { 7933 levelsSkipped.push(i); 7934 continue; 7935 } 7936 const levelDetails = levelInfo.details; 7937 const avgDuration = (partCurrent ? levelDetails == null ? void 0 : levelDetails.partTarget : levelDetails == null ? void 0 : levelDetails.averagetargetduration) || currentFragDuration; 7938 let adjustedbw; 7939 // follow algorithm captured from stagefright : 7940 // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp 7941 // Pick the highest bandwidth stream below or equal to estimated bandwidth. 7942 // consider only 80% of the available bandwidth, but if we are switching up, 7943 // be even more conservative (70%) to avoid overestimating and immediately 7944 // switching back. 7945 if (!upSwitch) { 7946 adjustedbw = bwFactor * currentBw; 7947 } else { 7948 adjustedbw = bwUpFactor * currentBw; 7949 } 7950 7951 // Use average bitrate when starvation delay (buffer length) is gt or eq tw
7951o segment durations and rebuffering is not expected (maxStarvationDelay > 0) 7952 const bitrate = currentFragDuration && bufferStarvationDelay >= currentFragDuration * 2 && maxStarvationDelay === 0 ? levels[i].averageBitrate : levels[i].maxBitrate; 7953 const fetchDuration = this.getTimeToLoadFrag(ttfbEstimateSec, adjustedbw, bitrate * avgDuration, levelDetails === undefined); 7954 const canSwitchWithinTolerance = 7955 // if adjusted bw is greater than level bitrate AND 7956 adjustedbw >= bitrate && ( 7957 // no level change, or new level has no error history 7958 i === lastLoadedFragLevel || levelInfo.loadError === 0 && levelInfo.fragmentError === 0) && ( 7959 // fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches 7960 // we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ... 7961 // special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1 7962 fetchDuration <= ttfbEstimateSec || !isFiniteNumber(fetchDuration) || live && !this.bitrateTestDelay || fetchDuration < maxFetchDuration); 7963 if (canSwitchWithinTolerance) { 7964 const forcedAutoLevel = this.forcedAutoLevel; 7965 if (i !== loadLevel && (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)) { 7966 if (levelsSkipped.length) { 7967 logger.trace(`[abr] Skipped level(s) ${levelsSkipped.join(',')} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${levels[levelsSkipped[0]].codecs}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${level.codecs}" ${currentVideoRange}`); 7968 } 7969 logger.info(`[abr] switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(adjustedbw)})-bitrate=${Math.round(adjustedbw - bitrate)} ttfb:${ttfbEstimateSec.toFixed(1)} avgDuration:${avgDuration.toFixed(1)} maxFetchDuration:${maxFetchDuration.toFixed(1)} fetchDuration:${fetchDuration.toFixed(1)} firstSelection:${firstSelection} codecSet:${currentCodecSet} videoRange:${currentVideoRange} hls.loadLevel:${loadLevel}`); 7970 } 7971 if (firstSelection) { 7972 this.firstSelection = i; 7973 } 7974 // as we are looping from highest to lowest, this will return the best achievable quality level 7975 return i; 7976 } 7977 } 7978 // not enough time budget even with quality level 0 ... rebuffering might happen 7979 return -1; 7980 } 7981 set nextAutoLevel(nextLevel) { 7982 const { 7983 maxAutoLevel, 7984 minAutoLevel 7985 } = this.hls; 7986 const value = Math.min(Math.max(nextLevel, minAutoLevel), maxAutoLevel); 7987 if (this._nextAutoLevel !== value) { 7988 this.nextAutoLevelKey = ''; 7989 this._nextAutoLevel = value; 7990 } 7991 } 7992} 7993 7994/** 7995 * @ignore 7996 * Sub-class specialization of EventHandler base class. 7997 * 7998 * TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop, 7999 * scheduled asynchroneously, avoiding recursive calls in the same tick. 8000 * 8001 * The task itself is implemented in `doTick`. It can be requested and called for single execution 8002 * using the `tick` method. 8003 * 8004 * It will be assured that the task execution method (`tick`) only gets called once per main loop "tick", 8005 * no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly. 8006 * 8007 * If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`, 8008 * and cancelled with `clearNextTick`. 8009 * 8010 * The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`). 8011 * 8012 * Sub-classes need to implement the `doTick` method which will effectively have the task execution routine. 8013 * 8014 * Further explanations: 8015 * 8016 * The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously 8017 * only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks. 8018 *
8019 * When the task execution (`tick` method) is called in re-entrant way this is detected and 8020 * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further 8021 * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo). 8022 */ 8023class TaskLoop { 8024 constructor() { 8025 this._boundTick = void 0; 8026 this._tickTimer = null; 8027 this._tickInterval = null; 8028 this._tickCallCount = 0; 8029 this._boundTick = this.tick.bind(this); 8030 } 8031 destroy() { 8032 this.onHandlerDestroying(); 8033 this.onHandlerDestroyed(); 8034 } 8035 onHandlerDestroying() { 8036 // clear all timers before unregistering from event bus 8037 this.clearNextTick(); 8038 this.clearInterval(); 8039 } 8040 onHandlerDestroyed() {} 8041 hasInterval() { 8042 return !!this._tickInterval; 8043 } 8044 hasNextTick() { 8045 return !!this._tickTimer; 8046 } 8047 8048 /** 8049 * @param millis - Interval time (ms) 8050 * @eturns True when interval has been scheduled, false when already scheduled (no effect) 8051 */ 8052 setInterval(millis) { 8053 if (!this._tickInterval) { 8054 this._tickCallCount = 0; 8055 this._tickInterval = self.setInterval(this._boundTick, millis); 8056 return true; 8057 } 8058 return false; 8059 } 8060 8061 /** 8062 * @returns True when interval was cleared, false when none was set (no effect) 8063 */ 8064 clearInterval() { 8065 if (this._tickInterval) { 8066 self.clearInterval(this._tickInterval); 8067 this._tickInterval = null; 8068 return true; 8069 } 8070 return false; 8071 } 8072 8073 /** 8074 * @returns True when timeout was cleared, false when none was set (no effect) 8075 */ 8076 clearNextTick() { 8077 if (this._tickTimer) { 8078 self.clearTimeout(this._tickTimer); 8079 this._tickTimer = null; 8080 return true; 8081 } 8082 return false; 8083 } 8084 8085 /** 8086 * Will call the subclass doTick implementation in this main loop tick 8087 * or in the next one (via setTimeout(,0)) in case it has already been called 8088 * in this tick (in case this is a re-entrant call). 8089 */ 8090 tick() { 8091 this._tickCallCount++; 8092 if (this._tickCallCount === 1) { 8093 this.doTick(); 8094 // re-entrant call to tick from previous doTick call stack 8095 // -> schedule a call on the next main loop iteration to process this task processing request 8096 if (this._tickCallCount > 1) { 8097 // make sure only one timer exists at any time at max 8098 this.tickImmediate(); 8099 } 8100 this._tickCallCount = 0; 8101 } 8102 } 8103 tickImmediate() { 8104 this.clearNextTick(); 8105 this._tickTimer = self.setTimeout(this._boundTick, 0); 8106 } 8107 8108 /** 8109 * For subclass to implement task logic 8110 * @abstract 8111 */ 8112 doTick() {} 8113} 8114 8115var FragmentState = { 8116 NOT_LOADED: "NOT_LOADED", 8117 APPENDING: "APPENDING", 8118 PARTIAL: "PARTIAL", 8119 OK: "OK" 8120}; 8121class FragmentTracker { 8122 constructor(hls) { 8123 this.activePartLists = Object.create(null); 8124 this.endListFragments = Object.create(null); 8125 this.fragments = Object.create(null); 8126 this.timeRanges = Object.create(null); 8127 this.bufferPadding = 0.2; 8128 this.hls = void 0; 8129 this.hasGaps = false; 8130 this.hls = hls; 8131 this._registerListeners(); 8132 } 8133 _registerListeners() { 8134 const { 8135 hls 8136 } = this; 8137 hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); 8138 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 8139 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 8140 } 8141 _unregisterListeners() { 8142 const { 8143 hls 8144 } = this; 8145 hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); 8146 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 8147 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 8148 } 8149 destroy() { 8150 this._unregisterListeners(); 8151 // @ts-ignore 8152 this.fragments = 8153 // @ts-ignore 8154 this.activePartLists = 8155 // @ts-ignore 8156 this.endListFragments = this.timeRanges = null; 8157 } 8158 8159 /** 8160 * Return a Fragment or Part with an appended range that matches the position and levelType 8161 * Otherwise, return null 8162 */ 8163 getAppendedFrag(position, levelType) { 8164 const activeParts = this.activePartLists[levelType]; 8165 if (activeParts) { 8166 for (let i = activeParts.length; i--;) { 8167 const activePart = activeParts[i]; 8168 if (!activePart) { 8169 break; 8170 } 8171 const appendedPTS = activePart.end; 8172 if (activePart.start <= position && appendedPTS !== null && position <= appendedPTS) { 8173 return activePart; 8174 } 8175 } 8176 } 8177 return this.getBufferedFrag(position, levelType); 8178 } 8179 8180 /** 8181 * Return a buffered Fragment that matches the position and levelType. 8182 * A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted). 8183 * If not found any Fragment, return null 8184 */ 8185 getBufferedFrag(position, levelType) { 8186 const { 8187 fragments 8188 } = this;
vendor: 6,101 bytes, lines 8189-8366
8189 const keys = Object.keys(fragments); 8190 for (let i = keys.length; i--;) { 8191 const fragmentEntity = fragments[keys[i]]; 8192 if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === levelType && fragmentEntity.buffered) { 8193 const frag = fragmentEntity.body; 8194 if (frag.start <= position && position <= frag.end) { 8195 return frag; 8196 } 8197 } 8198 } 8199 return null; 8200 } 8201 8202 /** 8203 * Partial fragments effected by coded frame eviction will be removed 8204 * The browser will unload parts of the buffer to free up memory for new buffer data 8205 * Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable) 8206 */ 8207 detectEvictedFragments(elementaryStream, timeRange, playlistType, appendedPart) { 8208 if (this.timeRanges) { 8209 this.timeRanges[elementaryStream] = timeRange; 8210 } 8211 // Check if any flagged fragments have been unloaded 8212 // excluding anything newer than appendedPartSn 8213 const appendedPartSn = (appendedPart == null ? void 0 : appendedPart.fragment.sn) || -1; 8214 Object.keys(this.fragments).forEach(key => { 8215 const fragmentEntity = this.fragments[key]; 8216 if (!fragmentEntity) { 8217 return; 8218 } 8219 if (appendedPartSn >= fragmentEntity.body.sn) { 8220 return; 8221 } 8222 if (!fragmentEntity.buffered && !fragmentEntity.loaded) { 8223 if (fragmentEntity.body.type === playlistType) { 8224 this.removeFragment(fragmentEntity.body); 8225 } 8226 return; 8227 } 8228 const esData = fragmentEntity.range[elementaryStream]; 8229 if (!esData) { 8230 return; 8231 } 8232 esData.time.some(time => { 8233 const isNotBuffered = !this.isTimeBuffered(time.startPTS, time.endPTS, timeRange); 8234 if (isNotBuffered) { 8235 // Unregister partial fragment as it needs to load again to be reused 8236 this.removeFragment(fragmentEntity.body); 8237 } 8238 return isNotBuffered; 8239 }); 8240 }); 8241 } 8242 8243 /** 8244 * Checks if the fragment passed in is loaded in the buffer properly 8245 * Partially loaded fragments will be registered as a partial fragment 8246 */ 8247 detectPartialFragments(data) { 8248 const timeRanges = this.timeRanges; 8249 const { 8250 frag, 8251 part 8252 } = data; 8253 if (!timeRanges || frag.sn === 'initSegment') { 8254 return; 8255 } 8256 const fragKey = getFragmentKey(frag); 8257 const fragmentEntity = this.fragments[fragKey]; 8258 if (!fragmentEntity || fragmentEntity.buffered && frag.gap) { 8259 return; 8260 } 8261 const isFragHint = !frag.relurl; 8262 Object.keys(timeRanges).forEach(elementaryStream => { 8263 const streamInfo = frag.elementaryStreams[elementaryStream]; 8264 if (!streamInfo) { 8265 return; 8266 } 8267 const timeRange = timeRanges[elementaryStream]; 8268 const partial = isFragHint || streamInfo.partial === true; 8269 fragmentEntity.range[elementaryStream] = this.getBufferedTimes(frag, part, partial, timeRange); 8270 }); 8271 fragmentEntity.loaded = null; 8272 if (Object.keys(fragmentEntity.range).length) { 8273 fragmentEntity.buffered = true; 8274 const endList = fragmentEntity.body.endList = frag.endList || fragmentEntity.body.endList; 8275 if (endList) { 8276 this.endListFragments[fragmentEntity.body.type] = fragmentEntity; 8277 } 8278 if (!isPartial(fragmentEntity)) { 8279 // Remove older fragment parts from lookup after frag is tracked as buffered 8280 this.removeParts(frag.sn - 1, frag.type); 8281 } 8282 } else { 8283 // remove fragment if nothing was appended 8284 this.removeFragment(fragmentEntity.body); 8285 } 8286 } 8287 removeParts(snToKeep, levelType) { 8288 const activeParts = this.activePartLists[levelType]; 8289 if (!activeParts) { 8290 return; 8291 } 8292 this.activePartLists[levelType] = activeParts.filter(part => part.fragment.sn >= snToKeep); 8293 } 8294 fragBuffered(frag, force) { 8295 const fragKey = getFragmentKey(frag); 8296 let fragmentEntity = this.fragments[fragKey]; 8297 if (!fragmentEntity && force) { 8298 fragmentEntity = this.fragments[fragKey] = { 8299 body: frag, 8300 appendedPTS: null, 8301 loaded: null, 8302 buffered: false, 8303 range: Object.create(null) 8304 }; 8305 if (frag.gap) { 8306 this.hasGaps = true; 8307 } 8308 } 8309 if (fragmentEntity) { 8310 fragmentEntity.loaded = null; 8311 fragmentEntity.buffered = true; 8312 } 8313 } 8314 getBufferedTimes(fragment, part, partial, timeRange) { 8315 const buffered = { 8316 time: [], 8317 partial 8318 }; 8319 const startPTS = fragment.start; 8320 const endPTS = fragment.end; 8321 const minEndPTS = fragment.minEndPTS || endPTS; 8322 const maxStartPTS = fragment.maxStartPTS || startPTS; 8323 for (let i = 0; i < timeRange.length; i++) { 8324 const startTime = timeRange.start(i) - this.bufferPadding; 8325 const endTime = timeRange.end(i) + this.bufferPadding; 8326 if (maxStartPTS >= startTime && minEndPTS <= endTime) { 8327 // Fragment is entirely contained in buffer 8328 // No need to check the other timeRange times since it's completely playable 8329 buffered.time.push({ 8330 startPTS: Math.max(startPTS, timeRange.start(i)), 8331 endPTS: Math.min(endPTS, timeRange.end(i)) 8332 }); 8333 break; 8334 } else if (startPTS < endTime && endPTS > startTime) { 8335 const start = Math.max(startPTS, timeRange.start(i)); 8336 const end = Math.min(endPTS, timeRange.end(i)); 8337 if (end > start) { 8338 buffered.partial = true; 8339 // Check for intersection with buffer 8340 // Get playable sections of the fragment 8341 buffered.time.push({ 8342 startPTS: start, 8343 endPTS: end 8344 }); 8345 } 8346 } else if (endPTS <= startTime) { 8347 // No need to check the rest of the timeRange as it is in order 8348 break; 8349 } 8350 } 8351 return buffered; 8352 } 8353 8354 /** 8355 * Gets the partial fragment for a certain time 8356 */ 8357 getPartialFragment(time) { 8358 let bestFragment = null; 8359 let timePadding; 8360 let startTime; 8361 let endTime; 8362 let bestOverlap = 0; 8363 const { 8364 bufferPadding, 8365 fragments 8366 } = this;
vendor: 4,068 bytes, lines 8367-8496
8367 Object.keys(fragments).forEach(key => { 8368 const fragmentEntity = fragments[key]; 8369 if (!fragmentEntity) { 8370 return; 8371 } 8372 if (isPartial(fragmentEntity)) { 8373 startTime = fragmentEntity.body.start - bufferPadding; 8374 endTime = fragmentEntity.body.end + bufferPadding; 8375 if (time >= startTime && time <= endTime) { 8376 // Use the fragment that has the most padding from start and end time 8377 timePadding = Math.min(time - startTime, endTime - time); 8378 if (bestOverlap <= timePadding) { 8379 bestFragment = fragmentEntity.body; 8380 bestOverlap = timePadding; 8381 } 8382 } 8383 } 8384 }); 8385 return bestFragment; 8386 } 8387 isEndListAppended(type) { 8388 const lastFragmentEntity = this.endListFragments[type]; 8389 return lastFragmentEntity !== undefined && (lastFragmentEntity.buffered || isPartial(lastFragmentEntity)); 8390 } 8391 getState(fragment) { 8392 const fragKey = getFragmentKey(fragment); 8393 const fragmentEntity = this.fragments[fragKey]; 8394 if (fragmentEntity) { 8395 if (!fragmentEntity.buffered) { 8396 return FragmentState.APPENDING; 8397 } else if (isPartial(fragmentEntity)) { 8398 return FragmentState.PARTIAL; 8399 } else { 8400 return FragmentState.OK; 8401 } 8402 } 8403 return FragmentState.NOT_LOADED; 8404 } 8405 isTimeBuffered(startPTS, endPTS, timeRange) { 8406 let startTime; 8407 let endTime; 8408 for (let i = 0; i < timeRange.length; i++) { 8409 startTime = timeRange.start(i) - this.bufferPadding; 8410 endTime = timeRange.end(i) + this.bufferPadding; 8411 if (startPTS >= startTime && endPTS <= endTime) { 8412 return true; 8413 } 8414 if (endPTS <= startTime) { 8415 // No need to check the rest of the timeRange as it is in order 8416 return false; 8417 } 8418 } 8419 return false; 8420 } 8421 onFragLoaded(event, data) { 8422 const { 8423 frag, 8424 part 8425 } = data; 8426 // don't track initsegment (for which sn is not a number) 8427 // don't track frags used for bitrateTest, they're irrelevant. 8428 if (frag.sn === 'initSegment' || frag.bitrateTest) { 8429 return; 8430 } 8431 8432 // Fragment entity `loaded` FragLoadedData is null when loading parts 8433 const loaded = part ? null : data; 8434 const fragKey = getFragmentKey(frag); 8435 this.fragments[fragKey] = { 8436 body: frag, 8437 appendedPTS: null, 8438 loaded, 8439 buffered: false, 8440 range: Object.create(null) 8441 }; 8442 } 8443 onBufferAppended(event, data) { 8444 const { 8445 frag, 8446 part, 8447 timeRanges 8448 } = data; 8449 if (frag.sn === 'initSegment') { 8450 return; 8451 } 8452 const playlistType = frag.type; 8453 if (part) { 8454 let activeParts = this.activePartLists[playlistType]; 8455 if (!activeParts) { 8456 this.activePartLists[playlistType] = activeParts = []; 8457 } 8458 activeParts.push(part); 8459 } 8460 // Store the latest timeRanges loaded in the buffer 8461 this.timeRanges = timeRanges; 8462 Object.keys(timeRanges).forEach(elementaryStream => { 8463 const timeRange = timeRanges[elementaryStream]; 8464 this.detectEvictedFragments(elementaryStream, timeRange, playlistType, part); 8465 }); 8466 } 8467 onFragBuffered(event, data) { 8468 this.detectPartialFragments(data); 8469 } 8470 hasFragment(fragment) { 8471 const fragKey = getFragmentKey(fragment); 8472 return !!this.fragments[fragKey]; 8473 } 8474 hasParts(type) { 8475 var _this$activePartLists; 8476 return !!((_this$activePartLists = this.activePartLists[type]) != null && _this$activePartLists.length); 8477 } 8478 removeFragmentsInRange(start, end, playlistType, withGapOnly, unbufferedOnly) { 8479 if (withGapOnly && !this.hasGaps) { 8480 return; 8481 } 8482 Object.keys(this.fragments).forEach(key => { 8483 const fragmentEntity = this.fragments[key]; 8484 if (!fragmentEntity) { 8485 return; 8486 } 8487 const frag = fragmentEntity.body; 8488 if (frag.type !== playlistType || withGapOnly && !frag.gap) { 8489 return; 8490 } 8491 if (frag.start < end && frag.end > start && (fragmentEntity.buffered || unbufferedOnly)) { 8492 this.removeFragment(frag); 8493 } 8494 }); 8495 } 8496 removeFragment(fragment) {
8497 const fragKey = getFragmentKey(fragment); 8498 fragment.stats.loaded = 0; 8499 fragment.clearElementaryStreamInfo(); 8500 const activeParts = this.activePartLists[fragment.type]; 8501 if (activeParts) { 8502 const snToRemove = fragment.sn; 8503 this.activePartLists[fragment.type] = activeParts.filter(part => part.fragment.sn !== snToRemove); 8504 } 8505 delete this.fragments[fragKey]; 8506 if (fragment.endList) { 8507 delete this.endListFragments[fragment.type]; 8508 } 8509 } 8510 removeAllFragments() { 8511 this.fragments = Object.create(null); 8512 this.endListFragments = Object.create(null); 8513 this.activePartLists = Object.create(null); 8514 this.hasGaps = false; 8515 } 8516} 8517function isPartial(fragmentEntity) { 8518 var _fragmentEntity$range, _fragmentEntity$range2, _fragmentEntity$range3; 8519 return fragmentEntity.buffered && (fragmentEntity.body.gap || ((_fragmentEntity$range = fragmentEntity.range.video) == null ? void 0 : _fragmentEntity$range.partial) || ((_fragmentEntity$range2 = fragmentEntity.range.audio) == null ? void 0 : _fragmentEntity$range2.partial) || ((_fragmentEntity$range3 = fragmentEntity.range.audiovideo) == null ? void 0 : _fragmentEntity$range3.partial)); 8520} 8521function getFragmentKey(fragment) { 8522 return `${fragment.type}_${fragment.level}_${fragment.sn}`; 8523} 8524 8525/** 8526 * Provides methods dealing with buffer length retrieval for example. 8527 * 8528 * In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property. 8529 * 8530 * Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered 8531 */ 8532 8533const noopBuffered = { 8534 length: 0, 8535 start: () => 0, 8536 end: () => 0 8537}; 8538class BufferHelper { 8539 /** 8540 * Return true if `media`'s buffered include `position` 8541 */ 8542 static isBuffered(media, position) { 8543 try { 8544 if (media) { 8545 const buffered = BufferHelper.getBuffered(media); 8546 for (let i = 0; i < buffered.length; i++) { 8547 if (position >= buffered.start(i) && position <= buffered.end(i)) { 8548 return true; 8549 } 8550 } 8551 } 8552 } catch (error) { 8553 // this is to catch 8554 // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer': 8555 // This SourceBuffer has been removed from the parent media source 8556 } 8557 return false; 8558 } 8559 static bufferInfo(media, pos, maxHoleDuration) { 8560 try { 8561 if (media) { 8562 const vbuffered = BufferHelper.getBuffered(media); 8563 const buffered = []; 8564 let i; 8565 for (i = 0; i < vbuffered.length; i++) { 8566 buffered.push({ 8567 start: vbuffered.start(i), 8568 end: vbuffered.end(i) 8569 }); 8570 } 8571 return this.bufferedInfo(buffered, pos, maxHoleDuration); 8572 } 8573 } catch (error) { 8574 // this is to catch 8575 // InvalidStateError: Failed to read the 'buffered' property from 'SourceBuffer': 8576 // This SourceBuffer has been removed from the parent media source 8577 } 8578 return { 8579 len: 0, 8580 start: pos, 8581 end: pos, 8582 nextStart: undefined 8583 }; 8584 } 8585 static bufferedInfo(buffered, pos, maxHoleDuration) { 8586 pos = Math.max(0, pos); 8587 // sort on buffer.start/smaller end (IE does not always return sorted buffered range) 8588 buffered.sort(function (a, b) { 8589 const diff = a.start - b.start; 8590 if (diff) { 8591 return diff; 8592 } else { 8593 return b.end - a.end; 8594 } 8595 }); 8596 let buffered2 = []; 8597 if (maxHoleDuration) { 8598 // there might be some small holes between buffer time range 8599 // consider that holes smaller than maxHoleDuration are irrelevant and build another 8600 // buffer time range representations that discards those holes 8601 for (let i = 0; i < buffered.length; i++) { 8602 const buf2len = buffered2.length; 8603 if (buf2len) { 8604 const buf2end = buffered2[buf2len - 1].end; 8605 // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative) 8606 if (buffered[i].start - buf2end < maxHoleDuration) { 8607 // merge overlapping time ranges 8608 // update lastRange.end only if smaller than item.end 8609 // e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end) 8610 // whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15]) 8611 if (buffered[i].end > buf2end) { 8612 buffered2[buf2len - 1].end = buffered[i].end; 8613 } 8614 } else { 8615 // big hole 8616 buffered2.push(buffered[i]); 8617 } 8618 } else { 8619 // first value 8620 buffered2.push(buffered[i]); 8621 } 8622 } 8623 } else { 8624 buffered2 = buffered; 8625 } 8626 let bufferLen = 0; 8627 8628 // bufferStartNext can possibly be undefined based on the conditional logic below 8629 let bufferStartNext; 8630 8631 // bufferStart and bufferEnd are buffer boundaries around current video position 8632 let bufferStart = pos; 8633 let bufferEnd = pos; 8634 for (let i = 0; i < buffered2.length; i++) { 8635 const start = buffered2[i].start; 8636 const end = buffered2[i].end; 8637 // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i)); 8638 if (pos + maxHoleDuration >= start && pos < end) { 8639 // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length 8640 bufferStart = start; 8641 bufferEnd = end; 8642 bufferLen = bufferEnd - pos; 8643 } else if (pos + maxHoleDuration < start) { 8644 bufferStartNext = start; 8645 break; 8646 } 8647 } 8648 return { 8649 len: bufferLen, 8650 start: bufferStart || 0, 8651 end: bufferEnd || 0, 8652 nextStart: bufferStartNext 8653 }; 8654 } 8655 8656 /** 8657 * Safe method to get buffered property. 8658 * SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource 8659 */ 8660 static getBuffered(media) { 8661 try { 8662 return media.buffered; 8663 } catch (e) { 8664 logger.log('failed to get media.buffered', e); 8665 return noopBuffered; 8666 } 8667 } 8668} 8669 8670class ChunkMetadata { 8671 constructor(level, sn, id, size = 0, part = -1, partial = false) { 8672 this.level = void 0; 8673 this.sn = void 0; 8674 this.part = void 0; 8675 this.id = void 0; 8676 this.size = void 0; 8677 this.partial = void 0; 8678 this.transmuxing = getNewPerformanceTiming(); 8679 this.buffering = { 8680 audio: getNewPerformanceTiming(), 8681 video: getNewPerformanceTiming(), 8682 audiovideo: getNewPerformanceTiming() 8683 }; 8684 this.level = level; 8685 this.sn = sn; 8686 this.id = id; 8687 this.size = size; 8688 this.part = part; 8689 this.partial = partial; 8690 } 8691} 8692function getNewPerformanceTiming() { 8693 return { 8694 start: 0, 8695 executeStart: 0, 8696 executeEnd: 0, 8697 end: 0 8698 }; 8699} 8700 8701function findFirstFragWithCC(fragments, cc) { 8702 for (let i = 0, len = fragments.length; i < len; i++) { 8703 var _fragments$i; 8704 if (((_fragments$i = fragments[i]) == null ? void 0 : _fragments$i.cc) === cc) { 8705 return fragments[i]; 8706 } 8707 } 8708 return null; 8709} 8710function shouldAlignOnDiscontinuities(lastFrag, switchDetails, details) { 8711 if (switchDetails) { 8712 if (details.endCC > details.startCC || lastFrag && lastFrag.cc < details.startCC) { 8713 return true; 8714 } 8715 } 8716 return false;
8717} 8718 8719// Find the first frag in the previous level which matches the CC of the first frag of the new level 8720function findDiscontinuousReferenceFrag(prevDetails, curDetails) { 8721 const prevFrags = prevDetails.fragments; 8722 const curFrags = curDetails.fragments; 8723 if (!curFrags.length || !prevFrags.length) { 8724 logger.log('No fragments to align'); 8725 return; 8726 } 8727 const prevStartFrag = findFirstFragWithCC(prevFrags, curFrags[0].cc); 8728 if (!prevStartFrag || prevStartFrag && !prevStartFrag.startPTS) { 8729 logger.log('No frag in previous level to align on'); 8730 return; 8731 } 8732 return prevStartFrag; 8733} 8734function adjustFragmentStart(frag, sliding) { 8735 if (frag) { 8736 const start = frag.start + sliding; 8737 frag.start = frag.startPTS = start; 8738 frag.endPTS = start + frag.duration; 8739 } 8740} 8741function adjustSlidingStart(sliding, details) { 8742 // Update segments 8743 const fragments = details.fragments; 8744 for (let i = 0, len = fragments.length; i < len; i++) { 8745 adjustFragmentStart(fragments[i], sliding); 8746 } 8747 // Update LL-HLS parts at the end of the playlist 8748 if (details.fragmentHint) { 8749 adjustFragmentStart(details.fragmentHint, sliding); 8750 } 8751 details.alignedSliding = true; 8752} 8753 8754/** 8755 * Using the parameters of the last level, this function computes PTS' of the new fragments so that they form a 8756 * contiguous stream with the last fragments. 8757 * The PTS of a fragment lets Hls.js know where it fits into a stream - by knowing every PTS, we know which fragment to 8758 * download at any given time. PTS is normally computed when the fragment is demuxed, so taking this step saves us time 8759 * and an extra download. 8760 * @param lastFrag 8761 * @param lastLevel 8762 * @param details 8763 */ 8764function alignStream(lastFrag, switchDetails, details) { 8765 if (!switchDetails) { 8766 return; 8767 } 8768 alignDiscontinuities(lastFrag, details, switchDetails); 8769 if (!details.alignedSliding && switchDetails) { 8770 // If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level. 8771 // Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same 8772 // discontinuity sequence. 8773 alignMediaPlaylistByPDT(details, switchDetails); 8774 } 8775 if (!details.alignedSliding && switchDetails && !details.skippedSegments) { 8776 // Try to align on sn so that we pick a better start fragment. 8777 // Do not perform this on playlists with delta updates as this is only to align levels on switch 8778 // and adjustSliding only adjusts fragments after skippedSegments. 8779 adjustSliding(switchDetails, details); 8780 } 8781} 8782 8783/** 8784 * Computes the PTS if a new level's fragments using the PTS of a fragment in the last level which shares the same 8785 * discontinuity sequence. 8786 * @param lastFrag - The last Fragment which shares the same discontinuity sequence 8787 * @param lastLevel - The details of the last loaded level 8788 * @param details - The details of the new level 8789 */ 8790function alignDiscontinuities(lastFrag, details, switchDetails) { 8791 if (shouldAlignOnDiscontinuities(lastFrag, switchDetails, details)) { 8792 const referenceFrag = findDiscontinuousReferenceFrag(switchDetails, details); 8793 if (referenceFrag && isFiniteNumber(referenceFrag.start)) { 8794 logger.log(`Adjusting PTS using last level due to CC increase within current level ${details.url}`); 8795 adjustSlidingStart(referenceFrag.start, details); 8796 } 8797 } 8798} 8799 8800/** 8801 * Ensures appropriate time-alignment between renditions based on PDT. 8802 * This function assumes the timelines represented in `refDetails` are accurate, including the PDTs 8803 * for the last discontinuity sequence number shared by both playlists when present, 8804 * and uses the "wallclock"/PDT timeline as a cross-reference to `details`, adjusting the presentation 8805 * times/timelines of `details` accordingly. 8806 * Given the asynchronous nature of fetches and initial loads of live `main` and audio/subtitle tracks, 8807 * the primary purpose of this function is to ensure the "local timelines" of audio/subtitle tracks 8808 * are aligned to the main/video timeline, using PDT as the cross-reference/"anchor" that should 8809 * be consistent across playlists, per the HLS spec. 8810 * @param details - The details of the rendition you'd like to time-align (e.g. an audio rendition). 8811 * @param refDetails - The details of the reference rendition with start and PDT times for alignment. 8812 */ 8813function alignMediaPlaylistByPDT(details, refDetails) {
vendor: 9,821 bytes, lines 8814-9109
8814 if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) { 8815 return; 8816 } 8817 const fragments = details.fragments; 8818 const refFragments = refDetails.fragments; 8819 if (!fragments.length || !refFragments.length) { 8820 return; 8821 } 8822 8823 // Calculate a delta to apply to all fragments according to the delta in PDT times and start times 8824 // of a fragment in the reference details, and a fragment in the target details of the same discontinuity. 8825 // If a fragment of the same discontinuity was not found use the middle fragment of both. 8826 let refFrag; 8827 let frag; 8828 const targetCC = Math.min(refDetails.endCC, details.endCC); 8829 if (refDetails.startCC < targetCC && details.startCC < targetCC) { 8830 refFrag = findFirstFragWithCC(refFragments, targetCC); 8831 frag = findFirstFragWithCC(fragments, targetCC); 8832 } 8833 if (!refFrag || !frag) { 8834 refFrag = refFragments[Math.floor(refFragments.length / 2)]; 8835 frag = findFirstFragWithCC(fragments, refFrag.cc) || fragments[Math.floor(fragments.length / 2)]; 8836 } 8837 const refPDT = refFrag.programDateTime; 8838 const targetPDT = frag.programDateTime; 8839 if (!refPDT || !targetPDT) { 8840 return; 8841 } 8842 const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start); 8843 adjustSlidingStart(delta, details); 8844} 8845 8846const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb 8847 8848class FragmentLoader { 8849 constructor(config) { 8850 this.config = void 0; 8851 this.loader = null; 8852 this.partLoadTimeout = -1; 8853 this.config = config; 8854 } 8855 destroy() { 8856 if (this.loader) { 8857 this.loader.destroy(); 8858 this.loader = null; 8859 } 8860 } 8861 abort() { 8862 if (this.loader) { 8863 // Abort the loader for current fragment. Only one may load at any given time 8864 this.loader.abort(); 8865 } 8866 } 8867 load(frag, onProgress) { 8868 const url = frag.url; 8869 if (!url) { 8870 return Promise.reject(new LoadError({ 8871 type: ErrorTypes.NETWORK_ERROR, 8872 details: ErrorDetails.FRAG_LOAD_ERROR, 8873 fatal: false, 8874 frag, 8875 error: new Error(`Fragment does not have a ${url ? 'part list' : 'url'}`), 8876 networkDetails: null 8877 })); 8878 } 8879 this.abort(); 8880 const config = this.config; 8881 const FragmentILoader = config.fLoader; 8882 const DefaultILoader = config.loader; 8883 return new Promise((resolve, reject) => { 8884 if (this.loader) { 8885 this.loader.destroy(); 8886 } 8887 if (frag.gap) { 8888 if (frag.tagList.some(tags => tags[0] === 'GAP')) { 8889 reject(createGapLoadError(frag)); 8890 return; 8891 } else { 8892 // Reset temporary treatment as GAP tag 8893 frag.gap = false; 8894 } 8895 } 8896 const loader = this.loader = frag.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); 8897 const loaderContext = createLoaderContext(frag); 8898 const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); 8899 const loaderConfig = { 8900 loadPolicy, 8901 timeout: loadPolicy.maxLoadTimeMs, 8902 maxRetry: 0, 8903 retryDelay: 0, 8904 maxRetryDelay: 0, 8905 highWaterMark: frag.sn === 'initSegment' ? Infinity : MIN_CHUNK_SIZE 8906 }; 8907 // Assign frag stats to the loader's stats reference 8908 frag.stats = loader.stats; 8909 loader.load(loaderContext, loaderConfig, { 8910 onSuccess: (response, stats, context, networkDetails) => { 8911 this.resetLoader(frag, loader); 8912 let payload = response.data; 8913 if (context.resetIV && frag.decryptdata) { 8914 frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16)); 8915 payload = payload.slice(16); 8916 } 8917 resolve({ 8918 frag, 8919 part: null, 8920 payload, 8921 networkDetails 8922 }); 8923 }, 8924 onError: (response, context, networkDetails, stats) => { 8925 this.resetLoader(frag, loader); 8926 reject(new LoadError({ 8927 type: ErrorTypes.NETWORK_ERROR, 8928 details: ErrorDetails.FRAG_LOAD_ERROR, 8929 fatal: false, 8930 frag, 8931 response: _objectSpread2({ 8932 url, 8933 data: undefined 8934 }, response), 8935 error: new Error(`HTTP Error ${response.code} ${response.text}`), 8936 networkDetails, 8937 stats 8938 })); 8939 }, 8940 onAbort: (stats, context, networkDetails) => { 8941 this.resetLoader(frag, loader); 8942 reject(new LoadError({ 8943 type: ErrorTypes.NETWORK_ERROR, 8944 details: ErrorDetails.INTERNAL_ABORTED, 8945 fatal: false, 8946 frag, 8947 error: new Error('Aborted'), 8948 networkDetails, 8949 stats 8950 })); 8951 }, 8952 onTimeout: (stats, context, networkDetails) => { 8953 this.resetLoader(frag, loader); 8954 reject(new LoadError({ 8955 type: ErrorTypes.NETWORK_ERROR, 8956 details: ErrorDetails.FRAG_LOAD_TIMEOUT, 8957 fatal: false, 8958 frag, 8959 error: new Error(`Timeout after ${loaderConfig.timeout}ms`), 8960 networkDetails, 8961 stats 8962 })); 8963 }, 8964 onProgress: (stats, context, data, networkDetails) => { 8965 if (onProgress) { 8966 onProgress({ 8967 frag, 8968 part: null, 8969 payload: data, 8970 networkDetails 8971 }); 8972 } 8973 } 8974 }); 8975 }); 8976 } 8977 loadPart(frag, part, onProgress) { 8978 this.abort(); 8979 const config = this.config; 8980 const FragmentILoader = config.fLoader; 8981 const DefaultILoader = config.loader; 8982 return new Promise((resolve, reject) => { 8983 if (this.loader) { 8984 this.loader.destroy(); 8985 } 8986 if (frag.gap || part.gap) { 8987 reject(createGapLoadError(frag, part)); 8988 return; 8989 } 8990 const loader = this.loader = frag.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); 8991 const loaderContext = createLoaderContext(frag, part); 8992 // Should we define another load policy for parts? 8993 const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); 8994 const loaderConfig = { 8995 loadPolicy, 8996 timeout: loadPolicy.maxLoadTimeMs, 8997 maxRetry: 0, 8998 retryDelay: 0, 8999 maxRetryDelay: 0, 9000 highWaterMark: MIN_CHUNK_SIZE 9001 }; 9002 // Assign part stats to the loader's stats reference 9003 part.stats = loader.stats; 9004 loader.load(loaderContext, loaderConfig, { 9005 onSuccess: (response, stats, context, networkDetails) => { 9006 this.resetLoader(frag, loader); 9007 this.updateStatsFromPart(frag, part); 9008 const partLoadedData = { 9009 frag, 9010 part, 9011 payload: response.data, 9012 networkDetails 9013 }; 9014 onProgress(partLoadedData); 9015 resolve(partLoadedData); 9016 }, 9017 onError: (response, context, networkDetails, stats) => { 9018 this.resetLoader(frag, loader); 9019 reject(new LoadError({ 9020 type: ErrorTypes.NETWORK_ERROR, 9021 details: ErrorDetails.FRAG_LOAD_ERROR, 9022 fatal: false, 9023 frag, 9024 part, 9025 response: _objectSpread2({ 9026 url: loaderContext.url, 9027 data: undefined 9028 }, response), 9029 error: new Error(`HTTP Error ${response.code} ${response.text}`), 9030 networkDetails, 9031 stats 9032 })); 9033 }, 9034 onAbort: (stats, context, networkDetails) => { 9035 frag.stats.aborted = part.stats.aborted; 9036 this.resetLoader(frag, loader); 9037 reject(new LoadError({ 9038 type: ErrorTypes.NETWORK_ERROR, 9039 details: ErrorDetails.INTERNAL_ABORTED, 9040 fatal: false, 9041 frag, 9042 part, 9043 error: new Error('Aborted'), 9044 networkDetails, 9045 stats 9046 })); 9047 }, 9048 onTimeout: (stats, context, networkDetails) => { 9049 this.resetLoader(frag, loader); 9050 reject(new LoadError({ 9051 type: ErrorTypes.NETWORK_ERROR, 9052 details: ErrorDetails.FRAG_LOAD_TIMEOUT, 9053 fatal: false, 9054 frag, 9055 part, 9056 error: new Error(`Timeout after ${loaderConfig.timeout}ms`), 9057 networkDetails, 9058 stats 9059 })); 9060 } 9061 }); 9062 }); 9063 } 9064 updateStatsFromPart(frag, part) { 9065 const fragStats = frag.stats; 9066 const partStats = part.stats; 9067 const partTotal = partStats.total; 9068 fragStats.loaded += partStats.loaded; 9069 if (partTotal) { 9070 const estTotalParts = Math.round(frag.duration / part.duration); 9071 const estLoadedParts = Math.min(Math.round(fragStats.loaded / partTotal), estTotalParts); 9072 const estRemainingParts = estTotalParts - estLoadedParts; 9073 const estRemainingBytes = estRemainingParts * Math.round(fragStats.loaded / estLoadedParts); 9074 fragStats.total = fragStats.loaded + estRemainingBytes; 9075 } else { 9076 fragStats.total = Math.max(fragStats.loaded, fragStats.total); 9077 } 9078 const fragLoading = fragStats.loading; 9079 const partLoading = partStats.loading; 9080 if (fragLoading.start) { 9081 // add to fragment loader latency 9082 fragLoading.first += partLoading.first - partLoading.start; 9083 } else { 9084 fragLoading.start = partLoading.start; 9085 fragLoading.first = partLoading.first; 9086 } 9087 fragLoading.end = partLoading.end; 9088 } 9089 resetLoader(frag, loader) { 9090 frag.loader = null; 9091 if (this.loader === loader) { 9092 self.clearTimeout(this.partLoadTimeout); 9093 this.loader = null; 9094 } 9095 loader.destroy(); 9096 } 9097} 9098function createLoaderContext(frag, part = null) { 9099 const segment = part || frag; 9100 const loaderContext = { 9101 frag, 9102 part, 9103 responseType: 'arraybuffer', 9104 url: segment.url, 9105 headers: {}, 9106 rangeStart: 0, 9107 rangeEnd: 0 9108 }; 9109 const start = segment.byteRangeStartOffset;
9110 const end = segment.byteRangeEndOffset; 9111 if (isFiniteNumber(start) && isFiniteNumber(end)) { 9112 var _frag$decryptdata; 9113 let byteRangeStart = start; 9114 let byteRangeEnd = end; 9115 if (frag.sn === 'initSegment' && ((_frag$decryptdata = frag.decryptdata) == null ? void 0 : _frag$decryptdata.method) === 'AES-128') { 9116 // MAP segment encrypted with method 'AES-128', when served with HTTP Range, 9117 // has the unencrypted size specified in the range. 9118 // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6 9119 const fragmentLen = end - start; 9120 if (fragmentLen % 16) { 9121 byteRangeEnd = end + (16 - fragmentLen % 16); 9122 } 9123 if (start !== 0) { 9124 loaderContext.resetIV = true; 9125 byteRangeStart = start - 16; 9126 } 9127 } 9128 loaderContext.rangeStart = byteRangeStart; 9129 loaderContext.rangeEnd = byteRangeEnd; 9130 } 9131 return loaderContext; 9132} 9133function createGapLoadError(frag, part) { 9134 const error = new Error(`GAP ${frag.gap ? 'tag' : 'attribute'} found`); 9135 const errorData = { 9136 type: ErrorTypes.MEDIA_ERROR, 9137 details: ErrorDetails.FRAG_GAP, 9138 fatal: false, 9139 frag, 9140 error, 9141 networkDetails: null 9142 }; 9143 if (part) { 9144 errorData.part = part; 9145 } 9146 (part ? part : frag).stats.aborted = true; 9147 return new LoadError(errorData); 9148} 9149class LoadError extends Error { 9150 constructor(data) { 9151 super(data.error.message); 9152 this.data = void 0; 9153 this.data = data; 9154 } 9155} 9156 9157class AESCrypto { 9158 constructor(subtle, iv) { 9159 this.subtle = void 0; 9160 this.aesIV = void 0; 9161 this.subtle = subtle; 9162 this.aesIV = iv; 9163 } 9164 decrypt(data, key) { 9165 return this.subtle.decrypt({ 9166 name: 'AES-CBC', 9167 iv: this.aesIV 9168 }, key, data); 9169 } 9170} 9171 9172class FastAESKey { 9173 constructor(subtle, key) { 9174 this.subtle = void 0; 9175 this.key = void 0; 9176 this.subtle = subtle; 9177 this.key = key; 9178 } 9179 expandKey() { 9180 return this.subtle.importKey('raw', this.key, { 9181 name: 'AES-CBC' 9182 }, false, ['encrypt', 'decrypt']); 9183 } 9184} 9185 9186// PKCS7 9187function removePadding(array) { 9188 const outputBytes = array.byteLength; 9189 const paddingBytes = outputBytes && new DataView(array.buffer).getUint8(outputBytes - 1); 9190 if (paddingBytes) { 9191 return sliceUint8(array, 0, outputBytes - paddingBytes); 9192 } 9193 return array; 9194} 9195class AESDecryptor { 9196 constructor() { 9197 this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; 9198 this.subMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; 9199 this.invSubMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; 9200 this.sBox = new Uint32Array(256); 9201 this.invSBox = new Uint32Array(256); 9202 this.key = new Uint32Array(0); 9203 this.ksRows = 0; 9204 this.keySize = 0; 9205 this.keySchedule = void 0; 9206 this.invKeySchedule = void 0; 9207 this.initTable(); 9208 } 9209 9210 // Using view.getUint32() also swaps the byte order. 9211 uint8ArrayToUint32Array_(arrayBuffer) { 9212 const view = new DataView(arrayBuffer); 9213 const newArray = new Uint32Array(4); 9214 for (let i = 0; i < 4; i++) { 9215 newArray[i] = view.getUint32(i * 4); 9216 } 9217 return newArray; 9218 } 9219 initTable() { 9220 const sBox = this.sBox; 9221 const invSBox = this.invSBox; 9222 const subMix = this.subMix; 9223 const subMix0 = subMix[0]; 9224 const subMix1 = subMix[1]; 9225 const subMix2 = subMix[2]; 9226 const subMix3 = subMix[3]; 9227 const invSubMix = this.invSubMix; 9228 const invSubMix0 = invSubMix[0]; 9229 const invSubMix1 = invSubMix[1]; 9230 const invSubMix2 = invSubMix[2]; 9231 const invSubMix3 = invSubMix[3]; 9232 const d = new Uint32Array(256); 9233 let x = 0; 9234 let xi = 0; 9235 let i = 0; 9236 for (i = 0; i < 256; i++) { 9237 if (i < 128) { 9238 d[i] = i << 1; 9239 } else { 9240 d[i] = i << 1 ^ 0x11b; 9241 } 9242 } 9243 for (i = 0; i < 256; i++) { 9244 let sx = xi ^ xi << 1 ^ xi << 2 ^ xi << 3 ^ xi << 4; 9245 sx = sx >>> 8 ^ sx & 0xff ^ 0x63; 9246 sBox[x] = sx; 9247 invSBox[sx] = x; 9248 9249 // Compute multiplication 9250 const x2 = d[x]; 9251 const x4 = d[x2]; 9252 const x8 = d[x4]; 9253 9254 // Compute sub/invSub bytes, mix columns tables 9255 let t = d[sx] * 0x101 ^ sx * 0x1010100; 9256 subMix0[x] = t << 24 | t >>> 8; 9257 subMix1[x] = t << 16 | t >>> 16; 9258 subMix2[x] = t << 8 | t >>> 24; 9259 subMix3[x] = t; 9260 9261 // Compute inv sub bytes, inv mix columns tables
vendor: 6,515 bytes, lines 9262-9433
9262 t = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 9263 invSubMix0[sx] = t << 24 | t >>> 8; 9264 invSubMix1[sx] = t << 16 | t >>> 16; 9265 invSubMix2[sx] = t << 8 | t >>> 24; 9266 invSubMix3[sx] = t; 9267 9268 // Compute next counter 9269 if (!x) { 9270 x = xi = 1; 9271 } else { 9272 x = x2 ^ d[d[d[x8 ^ x2]]]; 9273 xi ^= d[d[xi]]; 9274 } 9275 } 9276 } 9277 expandKey(keyBuffer) { 9278 // convert keyBuffer to Uint32Array 9279 const key = this.uint8ArrayToUint32Array_(keyBuffer); 9280 let sameKey = true; 9281 let offset = 0; 9282 while (offset < key.length && sameKey) { 9283 sameKey = key[offset] === this.key[offset]; 9284 offset++; 9285 } 9286 if (sameKey) { 9287 return; 9288 } 9289 this.key = key; 9290 const keySize = this.keySize = key.length; 9291 if (keySize !== 4 && keySize !== 6 && keySize !== 8) { 9292 throw new Error('Invalid aes key size=' + keySize); 9293 } 9294 const ksRows = this.ksRows = (keySize + 6 + 1) * 4; 9295 let ksRow; 9296 let invKsRow; 9297 const keySchedule = this.keySchedule = new Uint32Array(ksRows); 9298 const invKeySchedule = this.invKeySchedule = new Uint32Array(ksRows); 9299 const sbox = this.sBox; 9300 const rcon = this.rcon; 9301 const invSubMix = this.invSubMix; 9302 const invSubMix0 = invSubMix[0]; 9303 const invSubMix1 = invSubMix[1]; 9304 const invSubMix2 = invSubMix[2]; 9305 const invSubMix3 = invSubMix[3]; 9306 let prev; 9307 let t; 9308 for (ksRow = 0; ksRow < ksRows; ksRow++) { 9309 if (ksRow < keySize) { 9310 prev = keySchedule[ksRow] = key[ksRow]; 9311 continue; 9312 } 9313 t = prev; 9314 if (ksRow % keySize === 0) { 9315 // Rot word 9316 t = t << 8 | t >>> 24; 9317 9318 // Sub word 9319 t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; 9320 9321 // Mix Rcon 9322 t ^= rcon[ksRow / keySize | 0] << 24; 9323 } else if (keySize > 6 && ksRow % keySize === 4) { 9324 // Sub word 9325 t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; 9326 } 9327 keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0; 9328 } 9329 for (invKsRow = 0; invKsRow < ksRows; invKsRow++) { 9330 ksRow = ksRows - invKsRow; 9331 if (invKsRow & 3) { 9332 t = keySchedule[ksRow]; 9333 } else { 9334 t = keySchedule[ksRow - 4]; 9335 } 9336 if (invKsRow < 4 || ksRow <= 4) { 9337 invKeySchedule[invKsRow] = t; 9338 } else { 9339 invKeySchedule[invKsRow] = invSubMix0[sbox[t >>> 24]] ^ invSubMix1[sbox[t >>> 16 & 0xff]] ^ invSubMix2[sbox[t >>> 8 & 0xff]] ^ invSubMix3[sbox[t & 0xff]]; 9340 } 9341 invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0; 9342 } 9343 } 9344 9345 // Adding this as a method greatly improves performance. 9346 networkToHostOrderSwap(word) { 9347 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 9348 } 9349 decrypt(inputArrayBuffer, offset, aesIV) { 9350 const nRounds = this.keySize + 6; 9351 const invKeySchedule = this.invKeySchedule; 9352 const invSBOX = this.invSBox; 9353 const invSubMix = this.invSubMix; 9354 const invSubMix0 = invSubMix[0]; 9355 const invSubMix1 = invSubMix[1]; 9356 const invSubMix2 = invSubMix[2]; 9357 const invSubMix3 = invSubMix[3]; 9358 const initVector = this.uint8ArrayToUint32Array_(aesIV); 9359 let initVector0 = initVector[0]; 9360 let initVector1 = initVector[1]; 9361 let initVector2 = initVector[2]; 9362 let initVector3 = initVector[3]; 9363 const inputInt32 = new Int32Array(inputArrayBuffer); 9364 const outputInt32 = new Int32Array(inputInt32.length); 9365 let t0, t1, t2, t3; 9366 let s0, s1, s2, s3; 9367 let inputWords0, inputWords1, inputWords2, inputWords3; 9368 let ksRow, i; 9369 const swapWord = this.networkToHostOrderSwap; 9370 while (offset < inputInt32.length) { 9371 inputWords0 = swapWord(inputInt32[offset]); 9372 inputWords1 = swapWord(inputInt32[offset + 1]); 9373 inputWords2 = swapWord(inputInt32[offset + 2]); 9374 inputWords3 = swapWord(inputInt32[offset + 3]); 9375 s0 = inputWords0 ^ invKeySchedule[0]; 9376 s1 = inputWords3 ^ invKeySchedule[1]; 9377 s2 = inputWords2 ^ invKeySchedule[2]; 9378 s3 = inputWords1 ^ invKeySchedule[3]; 9379 ksRow = 4; 9380 9381 // Iterate through the rounds of decryption 9382 for (i = 1; i < nRounds; i++) { 9383 t0 = invSubMix0[s0 >>> 24] ^ invSubMix1[s1 >> 16 & 0xff] ^ invSubMix2[s2 >> 8 & 0xff] ^ invSubMix3[s3 & 0xff] ^ invKeySchedule[ksRow]; 9384 t1 = invSubMix0[s1 >>> 24] ^ invSubMix1[s2 >> 16 & 0xff] ^ invSubMix2[s3 >> 8 & 0xff] ^ invSubMix3[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; 9385 t2 = invSubMix0[s2 >>> 24] ^ invSubMix1[s3 >> 16 & 0xff] ^ invSubMix2[s0 >> 8 & 0xff] ^ invSubMix3[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; 9386 t3 = invSubMix0[s3 >>> 24] ^ invSubMix1[s0 >> 16 & 0xff] ^ invSubMix2[s1 >> 8 & 0xff] ^ invSubMix3[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; 9387 // Update state 9388 s0 = t0; 9389 s1 = t1; 9390 s2 = t2; 9391 s3 = t3; 9392 ksRow = ksRow + 4; 9393 } 9394 9395 // Shift rows, sub bytes, add round key 9396 t0 = invSBOX[s0 >>> 24] << 24 ^ invSBOX[s1 >> 16 & 0xff] << 16 ^ invSBOX[s2 >> 8 & 0xff] << 8 ^ invSBOX[s3 & 0xff] ^ invKeySchedule[ksRow]; 9397 t1 = invSBOX[s1 >>> 24] << 24 ^ invSBOX[s2 >> 16 & 0xff] << 16 ^ invSBOX[s3 >> 8 & 0xff] << 8 ^ invSBOX[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; 9398 t2 = invSBOX[s2 >>> 24] << 24 ^ invSBOX[s3 >> 16 & 0xff] << 16 ^ invSBOX[s0 >> 8 & 0xff] << 8 ^ invSBOX[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; 9399 t3 = invSBOX[s3 >>> 24] << 24 ^ invSBOX[s0 >> 16 & 0xff] << 16 ^ invSBOX[s1 >> 8 & 0xff] << 8 ^ invSBOX[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; 9400 9401 // Write 9402 outputInt32[offset] = swapWord(t0 ^ initVector0); 9403 outputInt32[offset + 1] = swapWord(t3 ^ initVector1); 9404 outputInt32[offset + 2] = swapWord(t2 ^ initVector2); 9405 outputInt32[offset + 3] = swapWord(t1 ^ initVector3); 9406 9407 // reset initVector to last 4 unsigned int 9408 initVector0 = inputWords0; 9409 initVector1 = inputWords1; 9410 initVector2 = inputWords2; 9411 initVector3 = inputWords3; 9412 offset = offset + 4; 9413 } 9414 return outputInt32.buffer; 9415 } 9416} 9417 9418const CHUNK_SIZE = 16; // 16 bytes, 128 bits 9419 9420class Decrypter { 9421 constructor(config, { 9422 removePKCS7Padding = true 9423 } = {}) { 9424 this.logEnabled = true; 9425 this.removePKCS7Padding = void 0; 9426 this.subtle = null; 9427 this.softwareDecrypter = null; 9428 this.key = null; 9429 this.fastAesKey = null; 9430 this.remainderData = null; 9431 this.currentIV = null; 9432 this.currentResult = null; 9433 this.useSoftware = void 0;
9434 this.useSoftware = config.enableSoftwareAES; 9435 this.removePKCS7Padding = removePKCS7Padding; 9436 // built in decryptor expects PKCS7 padding 9437 if (removePKCS7Padding) { 9438 try { 9439 const browserCrypto = self.crypto; 9440 if (browserCrypto) { 9441 this.subtle = browserCrypto.subtle || browserCrypto.webkitSubtle; 9442 } 9443 } catch (e) { 9444 /* no-op */ 9445 } 9446 } 9447 this.useSoftware = !this.subtle; 9448 } 9449 destroy() { 9450 this.subtle = null; 9451 this.softwareDecrypter = null; 9452 this.key = null; 9453 this.fastAesKey = null; 9454 this.remainderData = null; 9455 this.currentIV = null; 9456 this.currentResult = null; 9457 } 9458 isSync() { 9459 return this.useSoftware; 9460 } 9461 flush() { 9462 const { 9463 currentResult, 9464 remainderData 9465 } = this; 9466 if (!currentResult || remainderData) { 9467 this.reset(); 9468 return null; 9469 } 9470 const data = new Uint8Array(currentResult); 9471 this.reset(); 9472 if (this.removePKCS7Padding) { 9473 return removePadding(data); 9474 } 9475 return data; 9476 } 9477 reset() { 9478 this.currentResult = null; 9479 this.currentIV = null; 9480 this.remainderData = null; 9481 if (this.softwareDecrypter) { 9482 this.softwareDecrypter = null; 9483 } 9484 } 9485 decrypt(data, key, iv) { 9486 if (this.useSoftware) { 9487 return new Promise((resolve, reject) => { 9488 this.softwareDecrypt(new Uint8Array(data), key, iv); 9489 const decryptResult = this.flush(); 9490 if (decryptResult) { 9491 resolve(decryptResult.buffer); 9492 } else { 9493 reject(new Error('[softwareDecrypt] Failed to decrypt data')); 9494 } 9495 }); 9496 } 9497 return this.webCryptoDecrypt(new Uint8Array(data), key, iv); 9498 } 9499 9500 // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached 9501 // data is handled in the flush() call 9502 softwareDecrypt(data, key, iv) { 9503 const { 9504 currentIV, 9505 currentResult, 9506 remainderData 9507 } = this; 9508 this.logOnce('JS AES decrypt'); 9509 // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call 9510 // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached 9511 // the end on flush(), but by that time we have already received all bytes for the segment. 9512 // Progressive decryption does not work with WebCrypto 9513 9514 if (remainderData) { 9515 data = appendUint8Array(remainderData, data); 9516 this.remainderData = null; 9517 } 9518 9519 // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes) 9520 const currentChunk = this.getValidChunk(data); 9521 if (!currentChunk.length) { 9522 return null; 9523 } 9524 if (currentIV) { 9525 iv = currentIV; 9526 } 9527 let softwareDecrypter = this.softwareDecrypter; 9528 if (!softwareDecrypter) { 9529 softwareDecrypter = this.softwareDecrypter = new AESDecryptor(); 9530 } 9531 softwareDecrypter.expandKey(key); 9532 const result = currentResult; 9533 this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv); 9534 this.currentIV = sliceUint8(currentChunk, -16).buffer; 9535 if (!result) { 9536 return null; 9537 } 9538 return result; 9539 } 9540 webCryptoDecrypt(data, key, iv) { 9541 if (this.key !== key || !this.fastAesKey) { 9542 if (!this.subtle) { 9543 return Promise.resolve(this.onWebCryptoError(data, key, iv)); 9544 } 9545 this.key = key; 9546 this.fastAesKey = new FastAESKey(this.subtle, key); 9547 } 9548 return this.fastAesKey.expandKey().then(aesKey => { 9549 // decrypt using web crypto 9550 if (!this.subtle) { 9551 return Promise.reject(new Error('web crypto not initialized')); 9552 } 9553 this.logOnce('WebCrypto AES decrypt'); 9554 const crypto = new AESCrypto(this.subtle, new Uint8Array(iv)); 9555 return crypto.decrypt(data.buffer, aesKey); 9556 }).catch(err => { 9557 logger.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`); 9558 return this.onWebCryptoError(data, key, iv); 9559 }); 9560 } 9561 onWebCryptoError(data, key, iv) { 9562 this.useSoftware = true; 9563 this.logEnabled = true; 9564 this.softwareDecrypt(data, key, iv); 9565 const decryptResult = this.flush(); 9566 if (decryptResult) { 9567 return decryptResult.buffer; 9568 } 9569 throw new Error('WebCrypto and softwareDecrypt: failed to decrypt data'); 9570 } 9571 getValidChunk(data) {
9572 let currentChunk = data; 9573 const splitPoint = data.length - data.length % CHUNK_SIZE; 9574 if (splitPoint !== data.length) { 9575 currentChunk = sliceUint8(data, 0, splitPoint); 9576 this.remainderData = sliceUint8(data, splitPoint); 9577 } 9578 return currentChunk; 9579 } 9580 logOnce(msg) { 9581 if (!this.logEnabled) { 9582 return; 9583 } 9584 logger.log(`[decrypter]: ${msg}`); 9585 this.logEnabled = false; 9586 } 9587} 9588 9589/** 9590 * TimeRanges to string helper 9591 */ 9592 9593const TimeRanges = { 9594 toString: function (r) { 9595 let log = ''; 9596 const len = r.length; 9597 for (let i = 0; i < len; i++) { 9598 log += `[${r.start(i).toFixed(3)}-${r.end(i).toFixed(3)}]`; 9599 } 9600 return log; 9601 } 9602}; 9603 9604const State = { 9605 STOPPED: 'STOPPED', 9606 IDLE: 'IDLE', 9607 KEY_LOADING: 'KEY_LOADING', 9608 FRAG_LOADING: 'FRAG_LOADING', 9609 FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY', 9610 WAITING_TRACK: 'WAITING_TRACK', 9611 PARSING: 'PARSING', 9612 PARSED: 'PARSED', 9613 ENDED: 'ENDED', 9614 ERROR: 'ERROR', 9615 WAITING_INIT_PTS: 'WAITING_INIT_PTS', 9616 WAITING_LEVEL: 'WAITING_LEVEL' 9617}; 9618class BaseStreamController extends TaskLoop { 9619 constructor(hls, fragmentTracker, keyLoader, logPrefix, playlistType) { 9620 super(); 9621 this.hls = void 0; 9622 this.fragPrevious = null; 9623 this.fragCurrent = null; 9624 this.fragmentTracker = void 0; 9625 this.transmuxer = null; 9626 this._state = State.STOPPED; 9627 this.playlistType = void 0; 9628 this.media = null; 9629 this.mediaBuffer = null; 9630 this.config = void 0; 9631 this.bitrateTest = false; 9632 this.lastCurrentTime = 0; 9633 this.nextLoadPosition = 0; 9634 this.startPosition = 0; 9635 this.startTimeOffset = null; 9636 this.loadedmetadata = false; 9637 this.retryDate = 0; 9638 this.levels = null; 9639 this.fragmentLoader = void 0; 9640 this.keyLoader = void 0; 9641 this.levelLastLoaded = null; 9642 this.startFragRequested = false; 9643 this.decrypter = void 0; 9644 this.initPTS = []; 9645 this.onvseeking = null; 9646 this.onvended = null; 9647 this.logPrefix = ''; 9648 this.log = void 0; 9649 this.warn = void 0; 9650 this.playlistType = playlistType; 9651 this.logPrefix = logPrefix; 9652 this.log = logger.log.bind(logger, `${logPrefix}:`); 9653 this.warn = logger.warn.bind(logger, `${logPrefix}:`); 9654 this.hls = hls; 9655 this.fragmentLoader = new FragmentLoader(hls.config); 9656 this.keyLoader = keyLoader; 9657 this.fragmentTracker = fragmentTracker; 9658 this.config = hls.config; 9659 this.decrypter = new Decrypter(hls.config); 9660 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 9661 } 9662 doTick() { 9663 this.onTickEnd(); 9664 } 9665 onTickEnd() {} 9666 9667 // eslint-disable-next-line @typescript-eslint/no-unused-vars 9668 startLoad(startPosition) {} 9669 stopLoad() { 9670 this.fragmentLoader.abort(); 9671 this.keyLoader.abort(this.playlistType); 9672 const frag = this.fragCurrent; 9673 if (frag != null && frag.loader) { 9674 frag.abortRequests(); 9675 this.fragmentTracker.removeFragment(frag); 9676 } 9677 this.resetTransmuxer(); 9678 this.fragCurrent = null; 9679 this.fragPrevious = null; 9680 this.clearInterval(); 9681 this.clearNextTick(); 9682 this.state = State.STOPPED; 9683 } 9684 _streamEnded(bufferInfo, levelDetails) { 9685 // If playlist is live, there is another buffered range after the current range, nothing buffered, media is detached, 9686 // of nothing loading/loaded return false 9687 if (levelDetails.live || bufferInfo.nextStart || !bufferInfo.end || !this.media) { 9688 return false; 9689 } 9690 const partList = levelDetails.partList; 9691 // Since the last part isn't guaranteed to correspond to the last playlist segment for Low-Latency HLS, 9692 // check instead if the last part is buffered. 9693 if (partList != null && partList.length) { 9694 const lastPart = partList[partList.length - 1]; 9695 9696 // Checking the midpoint of the part for potential margin of error and related issues. 9697 // NOTE: Technically I believe parts could yield content that is < the computed duration (including potential a duration of 0) 9698 // and still be spec-compliant, so there may still be edge cases here. Likewise, there could be issues in end of stream 9699 // part mismatches for independent audio and video playlists/segments. 9700 const lastPartBuffered = BufferHelper.isBuffered(this.media, lastPart.start + lastPart.duration / 2); 9701 return lastPartBuffered; 9702 } 9703 const playlistType = levelDetails.fragments[levelDetails.fragments.length - 1].type; 9704 return this.fragmentTracker.isEndListAppended(playlistType); 9705 } 9706 getLevelDetails() { 9707 if (this.levels && this.levelLastLoaded !== null) { 9708 var _this$levelLastLoaded; 9709 return (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details; 9710 } 9711 } 9712 onMediaAttached(event, data) { 9713 const media = this.media = this.mediaBuffer = data.media; 9714 this.onvseeking = this.onMediaSeeking.bind(this); 9715 this.onvended = this.onMediaEnded.bind(this); 9716 media.addEventListener('seeking', this.onvseeking); 9717 media.addEventListener('ended', this.onvended); 9718 const config = this.config; 9719 if (this.levels && config.autoStartLoad && this.state === State.STOPPED) { 9720 this.startLoad(config.startPosition); 9721 } 9722 } 9723 onMediaDetaching() { 9724 const media = this.media; 9725 if (media != null && media.ended) { 9726 this.log('MSE detaching and video ended, reset startPosition'); 9727 this.startPosition = this.lastCurrentTime = 0; 9728 } 9729 9730 // remove video listeners 9731 if (media && this.onvseeking && this.onvended) { 9732 media.removeEventListener('seeking', this.onvseeking); 9733 media.removeEventListener('ended', this.onvended); 9734 this.onvseeking = this.onvended = null; 9735 } 9736 if (this.keyLoader) { 9737 this.keyLoader.detach(); 9738 } 9739 this.media = this.mediaBuffer = null; 9740 this.loadedmetadata = false;
vendor: 18,243 bytes, lines 9741-10192
9741 this.fragmentTracker.removeAllFragments(); 9742 this.stopLoad(); 9743 } 9744 onMediaSeeking() { 9745 const { 9746 config, 9747 fragCurrent, 9748 media, 9749 mediaBuffer, 9750 state 9751 } = this; 9752 const currentTime = media ? media.currentTime : 0; 9753 const bufferInfo = BufferHelper.bufferInfo(mediaBuffer ? mediaBuffer : media, currentTime, config.maxBufferHole); 9754 this.log(`media seeking to ${isFiniteNumber(currentTime) ? currentTime.toFixed(3) : currentTime}, state: ${state}`); 9755 if (this.state === State.ENDED) { 9756 this.resetLoadingState(); 9757 } else if (fragCurrent) { 9758 // Seeking while frag load is in progress 9759 const tolerance = config.maxFragLookUpTolerance; 9760 const fragStartOffset = fragCurrent.start - tolerance; 9761 const fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance; 9762 // if seeking out of buffered range or into new one 9763 if (!bufferInfo.len || fragEndOffset < bufferInfo.start || fragStartOffset > bufferInfo.end) { 9764 const pastFragment = currentTime > fragEndOffset; 9765 // if the seek position is outside the current fragment range 9766 if (currentTime < fragStartOffset || pastFragment) { 9767 if (pastFragment && fragCurrent.loader) { 9768 this.log('seeking outside of buffer while fragment load in progress, cancel fragment load'); 9769 fragCurrent.abortRequests(); 9770 this.resetLoadingState(); 9771 } 9772 this.fragPrevious = null; 9773 } 9774 } 9775 } 9776 if (media) { 9777 // Remove gap fragments 9778 this.fragmentTracker.removeFragmentsInRange(currentTime, Infinity, this.playlistType, true); 9779 this.lastCurrentTime = currentTime; 9780 } 9781 9782 // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target 9783 if (!this.loadedmetadata && !bufferInfo.len) { 9784 this.nextLoadPosition = this.startPosition = currentTime; 9785 } 9786 9787 // Async tick to speed up processing 9788 this.tickImmediate(); 9789 } 9790 onMediaEnded() { 9791 // reset startPosition and lastCurrentTime to restart playback @ stream beginning 9792 this.startPosition = this.lastCurrentTime = 0; 9793 } 9794 onManifestLoaded(event, data) { 9795 this.startTimeOffset = data.startTimeOffset; 9796 this.initPTS = []; 9797 } 9798 onHandlerDestroying() { 9799 this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 9800 this.stopLoad(); 9801 super.onHandlerDestroying(); 9802 // @ts-ignore 9803 this.hls = null; 9804 } 9805 onHandlerDestroyed() { 9806 this.state = State.STOPPED; 9807 if (this.fragmentLoader) { 9808 this.fragmentLoader.destroy(); 9809 } 9810 if (this.keyLoader) { 9811 this.keyLoader.destroy(); 9812 } 9813 if (this.decrypter) { 9814 this.decrypter.destroy(); 9815 } 9816 this.hls = this.log = this.warn = this.decrypter = this.keyLoader = this.fragmentLoader = this.fragmentTracker = null; 9817 super.onHandlerDestroyed(); 9818 } 9819 loadFragment(frag, level, targetBufferTime) { 9820 this._loadFragForPlayback(frag, level, targetBufferTime); 9821 } 9822 _loadFragForPlayback(frag, level, targetBufferTime) { 9823 const progressCallback = data => { 9824 if (this.fragContextChanged(frag)) { 9825 this.warn(`Fragment ${frag.sn}${data.part ? ' p: ' + data.part.index : ''} of level ${frag.level} was dropped during download.`); 9826 this.fragmentTracker.removeFragment(frag); 9827 return; 9828 } 9829 frag.stats.chunkCount++; 9830 this._handleFragmentLoadProgress(data); 9831 }; 9832 this._doFragLoad(frag, level, targetBufferTime, progressCallback).then(data => { 9833 if (!data) { 9834 // if we're here we probably needed to backtrack or are waiting for more parts 9835 return; 9836 } 9837 const state = this.state; 9838 if (this.fragContextChanged(frag)) { 9839 if (state === State.FRAG_LOADING || !this.fragCurrent && state === State.PARSING) { 9840 this.fragmentTracker.removeFragment(frag); 9841 this.state = State.IDLE; 9842 } 9843 return; 9844 } 9845 if ('payload' in data) { 9846 this.log(`Loaded fragment ${frag.sn} of level ${frag.level}`); 9847 this.hls.trigger(Events.FRAG_LOADED, data); 9848 } 9849 9850 // Pass through the whole payload; controllers not implementing progressive loading receive data from this callback 9851 this._handleFragmentLoadComplete(data); 9852 }).catch(reason => { 9853 if (this.state === State.STOPPED || this.state === State.ERROR) { 9854 return; 9855 } 9856 this.warn(`Frag error: ${(reason == null ? void 0 : reason.message) || reason}`); 9857 this.resetFragmentLoading(frag); 9858 }); 9859 } 9860 clearTrackerIfNeeded(frag) { 9861 var _this$mediaBuffer; 9862 const { 9863 fragmentTracker 9864 } = this; 9865 const fragState = fragmentTracker.getState(frag); 9866 if (fragState === FragmentState.APPENDING) { 9867 // Lower the max buffer length and try again 9868 const playlistType = frag.type; 9869 const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); 9870 const minForwardBufferLength = Math.max(frag.duration, bufferedInfo ? bufferedInfo.len : this.config.maxBufferLength); 9871 // If backtracking, always remove from the tracker without reducing max buffer length 9872 const backtrackFragment = this.backtrackFragment; 9873 const backtracked = backtrackFragment ? frag.sn - backtrackFragment.sn : 0; 9874 if (backtracked === 1 || this.reduceMaxBufferLength(minForwardBufferLength, frag.duration)) { 9875 fragmentTracker.removeFragment(frag); 9876 } 9877 } else if (((_this$mediaBuffer = this.mediaBuffer) == null ? void 0 : _this$mediaBuffer.buffered.length) === 0) { 9878 // Stop gap for bad tracker / buffer flush behavior 9879 fragmentTracker.removeAllFragments(); 9880 } else if (fragmentTracker.hasParts(frag.type)) { 9881 // In low latency mode, remove fragments for which only some parts were buffered 9882 fragmentTracker.detectPartialFragments({ 9883 frag, 9884 part: null, 9885 stats: frag.stats, 9886 id: frag.type 9887 }); 9888 if (fragmentTracker.getState(frag) === FragmentState.PARTIAL) { 9889 fragmentTracker.removeFragment(frag); 9890 } 9891 } 9892 } 9893 checkLiveUpdate(details) { 9894 if (details.updated && !details.live) { 9895 // Live stream ended, update fragment tracker 9896 const lastFragment = details.fragments[details.fragments.length - 1]; 9897 this.fragmentTracker.detectPartialFragments({ 9898 frag: lastFragment, 9899 part: null, 9900 stats: lastFragment.stats, 9901 id: lastFragment.type 9902 }); 9903 } 9904 if (!details.fragments[0]) { 9905 details.deltaUpdateFailed = true; 9906 } 9907 } 9908 flushMainBuffer(startOffset, endOffset, type = null) { 9909 if (!(startOffset - endOffset)) { 9910 return; 9911 } 9912 // When alternate audio is playing, the audio-stream-controller is responsible for the audio buffer. Otherwise, 9913 // passing a null type flushes both buffers 9914 const flushScope = { 9915 startOffset, 9916 endOffset, 9917 type 9918 }; 9919 this.hls.trigger(Events.BUFFER_FLUSHING, flushScope); 9920 } 9921 _loadInitSegment(frag, level) { 9922 this._doFragLoad(frag, level).then(data => { 9923 if (!data || this.fragContextChanged(frag) || !this.levels) { 9924 throw new Error('init load aborted'); 9925 } 9926 return data; 9927 }).then(data => { 9928 const { 9929 hls 9930 } = this; 9931 const { 9932 payload 9933 } = data; 9934 const decryptData = frag.decryptdata; 9935 9936 // check to see if the payload needs to be decrypted 9937 if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && decryptData.method === 'AES-128') { 9938 const startTime = self.performance.now(); 9939 // decrypt init segment data 9940 return this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer).catch(err => { 9941 hls.trigger(Events.ERROR, { 9942 type: ErrorTypes.MEDIA_ERROR, 9943 details: ErrorDetails.FRAG_DECRYPT_ERROR, 9944 fatal: false, 9945 error: err, 9946 reason: err.message, 9947 frag 9948 }); 9949 throw err; 9950 }).then(decryptedData => { 9951 const endTime = self.performance.now(); 9952 hls.trigger(Events.FRAG_DECRYPTED, { 9953 frag, 9954 payload: decryptedData, 9955 stats: { 9956 tstart: startTime, 9957 tdecrypt: endTime 9958 } 9959 }); 9960 data.payload = decryptedData; 9961 return this.completeInitSegmentLoad(data); 9962 }); 9963 } 9964 return this.completeInitSegmentLoad(data); 9965 }).catch(reason => { 9966 if (this.state === State.STOPPED || this.state === State.ERROR) { 9967 return; 9968 } 9969 this.warn(reason); 9970 this.resetFragmentLoading(frag); 9971 }); 9972 } 9973 completeInitSegmentLoad(data) { 9974 const { 9975 levels 9976 } = this; 9977 if (!levels) { 9978 throw new Error('init load aborted, missing levels'); 9979 } 9980 const stats = data.frag.stats; 9981 this.state = State.IDLE; 9982 data.frag.data = new Uint8Array(data.payload); 9983 stats.parsing.start = stats.buffering.start = self.performance.now(); 9984 stats.parsing.end = stats.buffering.end = self.performance.now(); 9985 this.tick(); 9986 } 9987 fragContextChanged(frag) { 9988 const { 9989 fragCurrent 9990 } = this; 9991 return !frag || !fragCurrent || frag.sn !== fragCurrent.sn || frag.level !== fragCurrent.level; 9992 } 9993 fragBufferedComplete(frag, part) { 9994 var _frag$startPTS, _frag$endPTS, _this$fragCurrent, _this$fragPrevious; 9995 const media = this.mediaBuffer ? this.mediaBuffer : this.media; 9996 this.log(`Buffered ${frag.type} sn: ${frag.sn}${part ? ' part: ' + part.index : ''} of ${this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'} ${frag.level} (frag:[${((_frag$startPTS = frag.startPTS) != null ? _frag$startPTS : NaN).toFixed(3)}-${((_frag$endPTS = frag.endPTS) != null ? _frag$endPTS : NaN).toFixed(3)}] > buffer:${media ? TimeRanges.toString(BufferHelper.getBuffered(media)) : '(detached)'})`); 9997 if (frag.sn !== 'initSegment') { 9998 var _this$levels; 9999 if (frag.type !== PlaylistLevelType.SUBTITLE) { 10000 const el = frag.elementaryStreams; 10001 if (!Object.keys(el).some(type => !!el[type])) { 10002 // empty segment 10003 this.state = State.IDLE; 10004 return; 10005 } 10006 } 10007 const level = (_this$levels = this.levels) == null ? void 0 : _this$levels[frag.level]; 10008 if (level != null && level.fragmentError) { 10009 this.log(`Resetting level fragment error count of ${level.fragmentError} on frag buffered`); 10010 level.fragmentError = 0; 10011 } 10012 } 10013 this.state = State.IDLE; 10014 if (!media) { 10015 return; 10016 } 10017 if (!this.loadedmetadata && frag.type == PlaylistLevelType.MAIN && media.buffered.length && ((_this$fragCurrent = this.fragCurrent) == null ? void 0 : _this$fragCurrent.sn) === ((_this$fragPrevious = this.fragPrevious) == null ? void 0 : _this$fragPrevious.sn)) { 10018 this.loadedmetadata = true; 10019 this.seekToStartPos(); 10020 } 10021 this.tick(); 10022 } 10023 seekToStartPos() {} 10024 _handleFragmentLoadComplete(fragLoadedEndData) { 10025 const { 10026 transmuxer 10027 } = this; 10028 if (!transmuxer) { 10029 return; 10030 } 10031 const { 10032 frag, 10033 part, 10034 partsLoaded 10035 } = fragLoadedEndData; 10036 // If we did not load parts, or loaded all parts, we have complete (not partial) fragment data 10037 const complete = !partsLoaded || partsLoaded.length === 0 || partsLoaded.some(fragLoaded => !fragLoaded); 10038 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount + 1, 0, part ? part.index : -1, !complete); 10039 transmuxer.flush(chunkMeta); 10040 } 10041 10042 // eslint-disable-next-line @typescript-eslint/no-unused-vars 10043 _handleFragmentLoadProgress(frag) {} 10044 _doFragLoad(frag, level, targetBufferTime = null, progressCallback) { 10045 var _frag$decryptdata; 10046 const details = level == null ? void 0 : level.details; 10047 if (!this.levels || !details) { 10048 throw new Error(`frag load aborted, missing level${details ? '' : ' detail'}s`); 10049 } 10050 let keyLoadingPromise = null; 10051 if (frag.encrypted && !((_frag$decryptdata = frag.decryptdata) != null && _frag$decryptdata.key)) { 10052 this.log(`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'} ${frag.level}`); 10053 this.state = State.KEY_LOADING; 10054 this.fragCurrent = frag; 10055 keyLoadingPromise = this.keyLoader.load(frag).then(keyLoadedData => { 10056 if (!this.fragContextChanged(keyLoadedData.frag)) { 10057 this.hls.trigger(Events.KEY_LOADED, keyLoadedData); 10058 if (this.state === State.KEY_LOADING) { 10059 this.state = State.IDLE; 10060 } 10061 return keyLoadedData; 10062 } 10063 }); 10064 this.hls.trigger(Events.KEY_LOADING, { 10065 frag 10066 }); 10067 if (this.fragCurrent === null) { 10068 keyLoadingPromise = Promise.reject(new Error(`frag load aborted, context changed in KEY_LOADING`)); 10069 } 10070 } else if (!frag.encrypted && details.encryptedFragments.length) { 10071 this.keyLoader.loadClear(frag, details.encryptedFragments); 10072 } 10073 targetBufferTime = Math.max(frag.start, targetBufferTime || 0); 10074 if (this.config.lowLatencyMode && frag.sn !== 'initSegment') { 10075 const partList = details.partList; 10076 if (partList && progressCallback) { 10077 if (targetBufferTime > frag.end && details.fragmentHint) { 10078 frag = details.fragmentHint; 10079 } 10080 const partIndex = this.getNextPart(partList, frag, targetBufferTime); 10081 if (partIndex > -1) { 10082 const part = partList[partIndex]; 10083 this.log(`Loading part sn: ${frag.sn} p: ${part.index} cc: ${frag.cc} of playlist [${details.startSN}-${details.endSN}] parts [0-${partIndex}-${partList.length - 1}] ${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`); 10084 this.nextLoadPosition = part.start + part.duration; 10085 this.state = State.FRAG_LOADING; 10086 let _result; 10087 if (keyLoadingPromise) { 10088 _result = keyLoadingPromise.then(keyLoadedData => { 10089 if (!keyLoadedData || this.fragContextChanged(keyLoadedData.frag)) { 10090 return null; 10091 } 10092 return this.doFragPartsLoad(frag, part, level, progressCallback); 10093 }).catch(error => this.handleFragLoadError(error)); 10094 } else { 10095 _result = this.doFragPartsLoad(frag, part, level, progressCallback).catch(error => this.handleFragLoadError(error)); 10096 } 10097 this.hls.trigger(Events.FRAG_LOADING, { 10098 frag, 10099 part, 10100 targetBufferTime 10101 }); 10102 if (this.fragCurrent === null) { 10103 return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING parts`)); 10104 } 10105 return _result; 10106 } else if (!frag.url || this.loadedEndOfParts(partList, targetBufferTime)) { 10107 // Fragment hint has no parts 10108 return Promise.resolve(null); 10109 } 10110 } 10111 } 10112 this.log(`Loading fragment ${frag.sn} cc: ${frag.cc} ${details ? 'of [' + details.startSN + '-' + details.endSN + '] ' : ''}${this.logPrefix === '[stream-controller]' ? 'level' : 'track'}: ${frag.level}, target: ${parseFloat(targetBufferTime.toFixed(3))}`); 10113 // Don't update nextLoadPosition for fragments which are not buffered 10114 if (isFiniteNumber(frag.sn) && !this.bitrateTest) { 10115 this.nextLoadPosition = frag.start + frag.duration; 10116 } 10117 this.state = State.FRAG_LOADING; 10118 10119 // Load key before streaming fragment data 10120 const dataOnProgress = this.config.progressive; 10121 let result; 10122 if (dataOnProgress && keyLoadingPromise) { 10123 result = keyLoadingPromise.then(keyLoadedData => { 10124 if (!keyLoadedData || this.fragContextChanged(keyLoadedData == null ? void 0 : keyLoadedData.frag)) { 10125 return null; 10126 } 10127 return this.fragmentLoader.load(frag, progressCallback); 10128 }).catch(error => this.handleFragLoadError(error)); 10129 } else { 10130 // load unencrypted fragment data with progress event, 10131 // or handle fragment result after key and fragment are finished loading 10132 result = Promise.all([this.fragmentLoader.load(frag, dataOnProgress ? progressCallback : undefined), keyLoadingPromise]).then(([fragLoadedData]) => { 10133 if (!dataOnProgress && fragLoadedData && progressCallback) { 10134 progressCallback(fragLoadedData); 10135 } 10136 return fragLoadedData; 10137 }).catch(error => this.handleFragLoadError(error)); 10138 } 10139 this.hls.trigger(Events.FRAG_LOADING, { 10140 frag, 10141 targetBufferTime 10142 }); 10143 if (this.fragCurrent === null) { 10144 return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING`)); 10145 } 10146 return result; 10147 } 10148 doFragPartsLoad(frag, fromPart, level, progressCallback) { 10149 return new Promise((resolve, reject) => { 10150 var _level$details; 10151 const partsLoaded = []; 10152 const initialPartList = (_level$details = level.details) == null ? void 0 : _level$details.partList; 10153 const loadPart = part => { 10154 this.fragmentLoader.loadPart(frag, part, progressCallback).then(partLoadedData => { 10155 partsLoaded[part.index] = partLoadedData; 10156 const loadedPart = partLoadedData.part; 10157 this.hls.trigger(Events.FRAG_LOADED, partLoadedData); 10158 const nextPart = getPartWith(level, frag.sn, part.index + 1) || findPart(initialPartList, frag.sn, part.index + 1); 10159 if (nextPart) { 10160 loadPart(nextPart); 10161 } else { 10162 return resolve({ 10163 frag, 10164 part: loadedPart, 10165 partsLoaded 10166 }); 10167 } 10168 }).catch(reject); 10169 }; 10170 loadPart(fromPart); 10171 }); 10172 } 10173 handleFragLoadError(error) { 10174 if ('data' in error) { 10175 const data = error.data; 10176 if (error.data && data.details === ErrorDetails.INTERNAL_ABORTED) { 10177 this.handleFragLoadAborted(data.frag, data.part); 10178 } else { 10179 this.hls.trigger(Events.ERROR, data); 10180 } 10181 } else { 10182 this.hls.trigger(Events.ERROR, { 10183 type: ErrorTypes.OTHER_ERROR, 10184 details: ErrorDetails.INTERNAL_EXCEPTION, 10185 err: error, 10186 error, 10187 fatal: true 10188 }); 10189 } 10190 return null; 10191 } 10192 _handleTransmuxerFlush(chunkMeta) {
10193 const context = this.getCurrentContext(chunkMeta); 10194 if (!context || this.state !== State.PARSING) { 10195 if (!this.fragCurrent && this.state !== State.STOPPED && this.state !== State.ERROR) { 10196 this.state = State.IDLE; 10197 } 10198 return; 10199 } 10200 const { 10201 frag, 10202 part, 10203 level 10204 } = context; 10205 const now = self.performance.now(); 10206 frag.stats.parsing.end = now; 10207 if (part) { 10208 part.stats.parsing.end = now; 10209 } 10210 this.updateLevelTiming(frag, part, level, chunkMeta.partial); 10211 } 10212 getCurrentContext(chunkMeta) { 10213 const { 10214 levels, 10215 fragCurrent 10216 } = this; 10217 const { 10218 level: levelIndex, 10219 sn, 10220 part: partIndex 10221 } = chunkMeta; 10222 if (!(levels != null && levels[levelIndex])) { 10223 this.warn(`Levels object was unset while buffering fragment ${sn} of level ${levelIndex}. The current chunk will not be buffered.`); 10224 return null; 10225 } 10226 const level = levels[levelIndex]; 10227 const part = partIndex > -1 ? getPartWith(level, sn, partIndex) : null; 10228 const frag = part ? part.fragment : getFragmentWithSN(level, sn, fragCurrent); 10229 if (!frag) { 10230 return null; 10231 } 10232 if (fragCurrent && fragCurrent !== frag) { 10233 frag.stats = fragCurrent.stats; 10234 } 10235 return { 10236 frag, 10237 part, 10238 level 10239 }; 10240 } 10241 bufferFragmentData(data, frag, part, chunkMeta, noBacktracking) { 10242 var _buffer; 10243 if (!data || this.state !== State.PARSING) { 10244 return; 10245 } 10246 const { 10247 data1, 10248 data2 10249 } = data; 10250 let buffer = data1; 10251 if (data1 && data2) { 10252 // Combine the moof + mdat so that we buffer with a single append 10253 buffer = appendUint8Array(data1, data2); 10254 } 10255 if (!((_buffer = buffer) != null && _buffer.length)) { 10256 return; 10257 } 10258 const segment = { 10259 type: data.type, 10260 frag, 10261 part, 10262 chunkMeta, 10263 parent: frag.type, 10264 data: buffer 10265 }; 10266 this.hls.trigger(Events.BUFFER_APPENDING, segment); 10267 if (data.dropped && data.independent && !part) { 10268 if (noBacktracking) { 10269 return; 10270 } 10271 // Clear buffer so that we reload previous segments sequentially if required 10272 this.flushBufferGap(frag); 10273 } 10274 } 10275 flushBufferGap(frag) { 10276 const media = this.media; 10277 if (!media) { 10278 return; 10279 } 10280 // If currentTime is not buffered, clear the back buffer so that we can backtrack as much as needed 10281 if (!BufferHelper.isBuffered(media, media.currentTime)) { 10282 this.flushMainBuffer(0, frag.start); 10283 return; 10284 } 10285 // Remove back-buffer without interrupting playback to allow back tracking 10286 const currentTime = media.currentTime; 10287 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 10288 const fragDuration = frag.duration; 10289 const segmentFraction = Math.min(this.config.maxFragLookUpTolerance * 2, fragDuration * 0.25); 10290 const start = Math.max(Math.min(frag.start - segmentFraction, bufferInfo.end - segmentFraction), currentTime + segmentFraction); 10291 if (frag.start - start > segmentFraction) { 10292 this.flushMainBuffer(start, frag.start); 10293 } 10294 } 10295 getFwdBufferInfo(bufferable, type) { 10296 const pos = this.getLoadPosition(); 10297 if (!isFiniteNumber(pos)) { 10298 return null; 10299 } 10300 return this.getFwdBufferInfoAtPos(bufferable, pos, type); 10301 } 10302 getFwdBufferInfoAtPos(bufferable, pos, type) { 10303 const { 10304 config: { 10305 maxBufferHole 10306 } 10307 } = this; 10308 const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole); 10309 // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos 10310 if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) { 10311 const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type); 10312 if (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) { 10313 return BufferHelper.bufferInfo(bufferable, pos, Math.max(bufferInfo.nextStart, maxBufferHole)); 10314 } 10315 } 10316 return bufferInfo; 10317 } 10318 getMaxBufferLength(levelBitrate) { 10319 const { 10320 config 10321 } = this; 10322 let maxBufLen; 10323 if (levelBitrate) { 10324 maxBufLen = Math.max(8 * config.maxBufferSize / levelBitrate, config.maxBufferLength); 10325 } else { 10326 maxBufLen = config.maxBufferLength; 10327 }
10328 return Math.min(maxBufLen, config.maxMaxBufferLength); 10329 } 10330 reduceMaxBufferLength(threshold, fragDuration) { 10331 const config = this.config; 10332 const minLength = Math.max(Math.min(threshold - fragDuration, config.maxBufferLength), fragDuration); 10333 const reducedLength = Math.max(threshold - fragDuration * 3, config.maxMaxBufferLength / 2, minLength); 10334 if (reducedLength >= minLength) { 10335 // reduce max buffer length as it might be too high. we do this to avoid loop flushing ... 10336 config.maxMaxBufferLength = reducedLength; 10337 this.warn(`Reduce max buffer length to ${reducedLength}s`); 10338 return true; 10339 } 10340 return false; 10341 } 10342 getAppendedFrag(position, playlistType = PlaylistLevelType.MAIN) { 10343 const fragOrPart = this.fragmentTracker.getAppendedFrag(position, PlaylistLevelType.MAIN); 10344 if (fragOrPart && 'fragment' in fragOrPart) { 10345 return fragOrPart.fragment; 10346 } 10347 return fragOrPart; 10348 } 10349 getNextFragment(pos, levelDetails) { 10350 const fragments = levelDetails.fragments; 10351 const fragLen = fragments.length; 10352 if (!fragLen) { 10353 return null; 10354 } 10355 10356 // find fragment index, contiguous with end of buffer position 10357 const { 10358 config 10359 } = this; 10360 const start = fragments[0].start; 10361 let frag; 10362 if (levelDetails.live) { 10363 const initialLiveManifestSize = config.initialLiveManifestSize; 10364 if (fragLen < initialLiveManifestSize) { 10365 this.warn(`Not enough fragments to start playback (have: ${fragLen}, need: ${initialLiveManifestSize})`); 10366 return null; 10367 } 10368 // The real fragment start times for a live stream are only known after the PTS range for that level is known. 10369 // In order to discover the range, we load the best matching fragment for that level and demux it. 10370 // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that 10371 // we get the fragment matching that start time 10372 if (!levelDetails.PTSKnown && !this.startFragRequested && this.startPosition === -1 || pos < start) { 10373 frag = this.getInitialLiveFragment(levelDetails, fragments); 10374 this.startPosition = this.nextLoadPosition = frag ? this.hls.liveSyncPosition || frag.start : pos; 10375 } 10376 } else if (pos <= start) { 10377 // VoD playlist: if loadPosition before start of playlist, load first fragment 10378 frag = fragments[0]; 10379 } 10380 10381 // If we haven't run into any special cases already, just load the fragment most closely matching the requested position 10382 if (!frag) { 10383 const end = config.lowLatencyMode ? levelDetails.partEnd : levelDetails.fragmentEnd; 10384 frag = this.getFragmentAtPosition(pos, end, levelDetails); 10385 } 10386 return this.mapToInitFragWhenRequired(frag); 10387 } 10388 isLoopLoading(frag, targetBufferTime) { 10389 const trackerState = this.fragmentTracker.getState(frag); 10390 return (trackerState === FragmentState.OK || trackerState === FragmentState.PARTIAL && !!frag.gap) && this.nextLoadPosition > targetBufferTime; 10391 } 10392 getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, playlistType, maxBufLen) { 10393 const gapStart = frag.gap; 10394 const nextFragment = this.getNextFragment(this.nextLoadPosition, levelDetails); 10395 if (nextFragment === null) { 10396 return nextFragment; 10397 } 10398 frag = nextFragment; 10399 if (gapStart && frag && !frag.gap && bufferInfo.nextStart) { 10400 // Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length 10401 const nextbufferInfo = this.getFwdBufferInfoAtPos(this.mediaBuffer ? this.mediaBuffer : this.media, bufferInfo.nextStart, playlistType); 10402 if (nextbufferInfo !== null && bufferInfo.len + nextbufferInfo.len >= maxBufLen) { 10403 // Returning here might result in not finding an audio and video candiate to skip to 10404 this.log(`buffer full after gaps in "${playlistType}" playlist starting at sn: ${frag.sn}`); 10405 return null; 10406 } 10407 } 10408 return frag; 10409 } 10410 mapToInitFragWhenRequired(frag) { 10411 // If an initSegment is present, it must be buffered first 10412 if (frag != null && frag.initSegment && !(frag != null && frag.initSegment.data) && !this.bitrateTest) { 10413 return frag.initSegment; 10414 } 10415 return frag; 10416 } 10417 getNextPart(partList, frag, targetBufferTime) { 10418 let nextPart = -1;
10419 let contiguous = false; 10420 let independentAttrOmitted = true; 10421 for (let i = 0, len = partList.length; i < len; i++) { 10422 const part = partList[i]; 10423 independentAttrOmitted = independentAttrOmitted && !part.independent; 10424 if (nextPart > -1 && targetBufferTime < part.start) { 10425 break; 10426 } 10427 const loaded = part.loaded; 10428 if (loaded) { 10429 nextPart = -1; 10430 } else if ((contiguous || part.independent || independentAttrOmitted) && part.fragment === frag) { 10431 nextPart = i; 10432 } 10433 contiguous = loaded; 10434 } 10435 return nextPart; 10436 } 10437 loadedEndOfParts(partList, targetBufferTime) { 10438 const lastPart = partList[partList.length - 1]; 10439 return lastPart && targetBufferTime > lastPart.start && lastPart.loaded; 10440 } 10441 10442 /* 10443 This method is used find the best matching first fragment for a live playlist. This fragment is used to calculate the 10444 "sliding" of the playlist, which is its offset from the start of playback. After sliding we can compute the real 10445 start and end times for each fragment in the playlist (after which this method will not need to be called). 10446 */ 10447 getInitialLiveFragment(levelDetails, fragments) { 10448 const fragPrevious = this.fragPrevious; 10449 let frag = null; 10450 if (fragPrevious) { 10451 if (levelDetails.hasProgramDateTime) { 10452 // Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding 10453 this.log(`Live playlist, switching playlist, load frag with same PDT: ${fragPrevious.programDateTime}`); 10454 frag = findFragmentByPDT(fragments, fragPrevious.endProgramDateTime, this.config.maxFragLookUpTolerance); 10455 } 10456 if (!frag) { 10457 // SN does not need to be accurate between renditions, but depending on the packaging it may be so. 10458 const targetSN = fragPrevious.sn + 1; 10459 if (targetSN >= levelDetails.startSN && targetSN <= levelDetails.endSN) { 10460 const fragNext = fragments[targetSN - levelDetails.startSN]; 10461 // Ensure that we're staying within the continuity range, since PTS resets upon a new range 10462 if (fragPrevious.cc === fragNext.cc) { 10463 frag = fragNext; 10464 this.log(`Live playlist, switching playlist, load frag with next SN: ${frag.sn}`); 10465 } 10466 } 10467 // It's important to stay within the continuity range if available; otherwise the fragments in the playlist 10468 // will have the wrong start times 10469 if (!frag) { 10470 frag = findFragWithCC(fragments, fragPrevious.cc); 10471 if (frag) { 10472 this.log(`Live playlist, switching playlist, load frag with same CC: ${frag.sn}`); 10473 } 10474 } 10475 } 10476 } else { 10477 // Find a new start fragment when fragPrevious is null 10478 const liveStart = this.hls.liveSyncPosition; 10479 if (liveStart !== null) { 10480 frag = this.getFragmentAtPosition(liveStart, this.bitrateTest ? levelDetails.fragmentEnd : levelDetails.edge, levelDetails); 10481 } 10482 } 10483 return frag; 10484 } 10485 10486 /* 10487 This method finds the best matching fragment given the provided position. 10488 */ 10489 getFragmentAtPosition(bufferEnd, end, levelDetails) { 10490 const { 10491 config 10492 } = this; 10493 let { 10494 fragPrevious 10495 } = this; 10496 let { 10497 fragments, 10498 endSN 10499 } = levelDetails; 10500 const { 10501 fragmentHint 10502 } = levelDetails; 10503 const { 10504 maxFragLookUpTolerance 10505 } = config; 10506 const partList = levelDetails.partList; 10507 const loadingParts = !!(config.lowLatencyMode && partList != null && partList.length && fragmentHint); 10508 if (loadingParts && fragmentHint && !this.bitrateTest) { 10509 // Include incomplete fragment with parts at end 10510 fragments = fragments.concat(fragmentHint); 10511 endSN = fragmentHint.sn; 10512 } 10513 let frag; 10514 if (bufferEnd < end) { 10515 const lookupTolerance = bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance; 10516 // Remove the tolerance if it would put the bufferEnd past the actual end of stream 10517 // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE) 10518 frag = findFragmentByPTS(fragPrevious, fragments, bufferEnd, lookupTolerance); 10519 } else { 10520 // reach end of playlist 10521 frag = fragments[fragments.length - 1]; 10522 } 10523 if (frag) { 10524 const curSNIdx = frag.sn - levelDetails.startSN; 10525 // Move fragPrevious forward to support forcing the next fragment to load
10526 // when the buffer catches up to a previously buffered range. 10527 const fragState = this.fragmentTracker.getState(frag); 10528 if (fragState === FragmentState.OK || fragState === FragmentState.PARTIAL && frag.gap) { 10529 fragPrevious = frag; 10530 } 10531 if (fragPrevious && frag.sn === fragPrevious.sn && (!loadingParts || partList[0].fragment.sn > frag.sn)) { 10532 // Force the next fragment to load if the previous one was already selected. This can occasionally happen with 10533 // non-uniform fragment durations 10534 const sameLevel = fragPrevious && frag.level === fragPrevious.level; 10535 if (sameLevel) { 10536 const nextFrag = fragments[curSNIdx + 1]; 10537 if (frag.sn < endSN && this.fragmentTracker.getState(nextFrag) !== FragmentState.OK) { 10538 frag = nextFrag; 10539 } else { 10540 frag = null; 10541 } 10542 } 10543 } 10544 } 10545 return frag; 10546 } 10547 synchronizeToLiveEdge(levelDetails) { 10548 const { 10549 config, 10550 media 10551 } = this; 10552 if (!media) { 10553 return; 10554 } 10555 const liveSyncPosition = this.hls.liveSyncPosition; 10556 const currentTime = media.currentTime; 10557 const start = levelDetails.fragments[0].start; 10558 const end = levelDetails.edge; 10559 const withinSlidingWindow = currentTime >= start - config.maxFragLookUpTolerance && currentTime <= end; 10560 // Continue if we can seek forward to sync position or if current time is outside of sliding window 10561 if (liveSyncPosition !== null && media.duration > liveSyncPosition && (currentTime < liveSyncPosition || !withinSlidingWindow)) { 10562 // Continue if buffer is starving or if current time is behind max latency 10563 const maxLatency = config.liveMaxLatencyDuration !== undefined ? config.liveMaxLatencyDuration : config.liveMaxLatencyDurationCount * levelDetails.targetduration; 10564 if (!withinSlidingWindow && media.readyState < 4 || currentTime < end - maxLatency) { 10565 if (!this.loadedmetadata) { 10566 this.nextLoadPosition = liveSyncPosition; 10567 } 10568 // Only seek if ready and there is not a significant forward buffer available for playback 10569 if (media.readyState) { 10570 this.warn(`Playback: ${currentTime.toFixed(3)} is located too far from the end of live sliding playlist: ${end}, reset currentTime to : ${liveSyncPosition.toFixed(3)}`); 10571 media.currentTime = liveSyncPosition; 10572 } 10573 } 10574 } 10575 } 10576 alignPlaylists(details, previousDetails, switchDetails) { 10577 // FIXME: If not for `shouldAlignOnDiscontinuities` requiring fragPrevious.cc, 10578 // this could all go in level-helper mergeDetails() 10579 const length = details.fragments.length; 10580 if (!length) { 10581 this.warn(`No fragments in live playlist`); 10582 return 0; 10583 } 10584 const slidingStart = details.fragments[0].start; 10585 const firstLevelLoad = !previousDetails; 10586 const aligned = details.alignedSliding && isFiniteNumber(slidingStart); 10587 if (firstLevelLoad || !aligned && !slidingStart) { 10588 const { 10589 fragPrevious 10590 } = this; 10591 alignStream(fragPrevious, switchDetails, details); 10592 const alignedSlidingStart = details.fragments[0].start; 10593 this.log(`Live playlist sliding: ${alignedSlidingStart.toFixed(2)} start-sn: ${previousDetails ? previousDetails.startSN : 'na'}->${details.startSN} prev-sn: ${fragPrevious ? fragPrevious.sn : 'na'} fragments: ${length}`); 10594 return alignedSlidingStart; 10595 } 10596 return slidingStart; 10597 } 10598 waitForCdnTuneIn(details) { 10599 // Wait for Low-Latency CDN Tune-in to get an updated playlist 10600 const advancePartLimit = 3; 10601 return details.live && details.canBlockReload && details.partTarget && details.tuneInGoal > Math.max(details.partHoldBack, details.partTarget * advancePartLimit); 10602 } 10603 setStartPosition(details, sliding) { 10604 // compute start position if set to -1. use it straight away if value is defined 10605 let startPosition = this.startPosition; 10606 if (startPosition < sliding) { 10607 startPosition = -1; 10608 } 10609 if (startPosition === -1 || this.lastCurrentTime === -1) { 10610 // Use Playlist EXT-X-START:TIME-OFFSET when set 10611 // Prioritize Multivariant Playlist offset so that main, audio, and subtitle stream-controller start times match 10612 const offsetInMultivariantPlaylist = this.startTimeOffset !== null; 10613 const startTimeOffset = offsetInMultivariantPlaylist ? this.startTimeOffset : details.startTimeOffset; 10614 if (startTimeOffset !== null && isFiniteNumber(startTimeOffset)) { 10615 startPosition = sliding + startTimeOffset; 10616 if (startTimeOffset < 0) { 10617 startPosition += details.totalduration; 10618 } 10619 startPosition = Math.min(Math.max(sliding, startPosition), sliding + details.totalduration); 10620 this.log(`Start time offset ${startTimeOffset} found in ${offsetInMultivariantPlaylist ? 'multivariant' : 'media'} playlist, adjust startPosition to ${startPosition}`); 10621 this.startPosition = startPosition; 10622 } else if (details.live) { 10623 // Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has 10624 // not been specified via the config or an as an argument to startLoad (#3736). 10625 startPosition = this.hls.liveSyncPosition || sliding; 10626 } else { 10627 this.startPosition = startPosition = 0; 10628 } 10629 this.lastCurrentTime = startPosition; 10630 } 10631 this.nextLoadPosition = startPosition; 10632 } 10633 getLoadPosition() { 10634 const { 10635 media 10636 } = this; 10637 // if we have not yet loaded any fragment, start loading from start position 10638 let pos = 0; 10639 if (this.loadedmetadata && media) { 10640 pos = media.currentTime; 10641 } else if (this.nextLoadPosition) { 10642 pos = this.nextLoadPosition; 10643 } 10644 return pos; 10645 } 10646 handleFragLoadAborted(frag, part) { 10647 if (this.transmuxer && frag.sn !== 'initSegment' && frag.stats.aborted) { 10648 this.warn(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${frag.level} was aborted`); 10649 this.resetFragmentLoading(frag); 10650 } 10651 } 10652 resetFragmentLoading(frag) { 10653 if (!this.fragCurrent || !this.fragContextChanged(frag) && this.state !== State.FRAG_LOADING_WAITING_RETRY) { 10654 this.state = State.IDLE; 10655 } 10656 } 10657 onFragmentOrKeyLoadError(filterType, data) { 10658 if (data.chunkMeta && !data.frag) { 10659 const context = this.getCurrentContext(data.chunkMeta); 10660 if (context) { 10661 data.frag = context.frag; 10662 } 10663 } 10664 const frag = data.frag; 10665 // Handle frag error related to caller's filterType 10666 if (!frag || frag.type !== filterType || !this.levels) { 10667 return; 10668 } 10669 if (this.fragContextChanged(frag)) { 10670 var _this$fragCurrent2; 10671 this.warn(`Frag load error must match current frag to retry ${frag.url} > ${(_this$fragCurrent2 = this.fragCurrent) == null ? void 0 : _this$fragCurrent2.url}`); 10672 return; 10673 } 10674 const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP; 10675 if (gapTagEncountered) { 10676 this.fragmentTracker.fragBuffered(frag, true); 10677 } 10678 // keep retrying until the limit will be reached 10679 const errorAction = data.errorAction; 10680 const { 10681 action, 10682 retryCount = 0, 10683 retryConfig 10684 } = errorAction || {}; 10685 if (errorAction && action === NetworkErrorAction.RetryRequest && retryConfig) { 10686 this.resetStartWhenNotLoaded(this.levelLastLoaded);
10687 const delay = getRetryDelay(retryConfig, retryCount); 10688 this.warn(`Fragment ${frag.sn} of ${filterType} ${frag.level} errored with ${data.details}, retrying loading ${retryCount + 1}/${retryConfig.maxNumRetry} in ${delay}ms`); 10689 errorAction.resolved = true; 10690 this.retryDate = self.performance.now() + delay; 10691 this.state = State.FRAG_LOADING_WAITING_RETRY; 10692 } else if (retryConfig && errorAction) { 10693 this.resetFragmentErrors(filterType); 10694 if (retryCount < retryConfig.maxNumRetry) { 10695 // Network retry is skipped when level switch is preferred 10696 if (!gapTagEncountered && action !== NetworkErrorAction.RemoveAlternatePermanently) { 10697 errorAction.resolved = true; 10698 } 10699 } else { 10700 logger.warn(`${data.details} reached or exceeded max retry (${retryCount})`); 10701 return; 10702 } 10703 } else if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox) { 10704 this.state = State.WAITING_LEVEL; 10705 } else { 10706 this.state = State.ERROR; 10707 } 10708 // Perform next async tick sooner to speed up error action resolution 10709 this.tickImmediate(); 10710 } 10711 reduceLengthAndFlushBuffer(data) { 10712 // if in appending state 10713 if (this.state === State.PARSING || this.state === State.PARSED) { 10714 const frag = data.frag; 10715 const playlistType = data.parent; 10716 const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); 10717 // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end 10718 // reduce max buf len if current position is buffered 10719 const buffered = bufferedInfo && bufferedInfo.len > 0.5; 10720 if (buffered) { 10721 this.reduceMaxBufferLength(bufferedInfo.len, (frag == null ? void 0 : frag.duration) || 10); 10722 } 10723 const flushBuffer = !buffered; 10724 if (flushBuffer) { 10725 // current position is not buffered, but browser is still complaining about buffer full error 10726 // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 10727 // in that case flush the whole audio buffer to recover 10728 this.warn(`Buffer full error while media.currentTime is not buffered, flush ${playlistType} buffer`); 10729 } 10730 if (frag) { 10731 this.fragmentTracker.removeFragment(frag); 10732 this.nextLoadPosition = frag.start; 10733 } 10734 this.resetLoadingState(); 10735 return flushBuffer; 10736 } 10737 return false; 10738 } 10739 resetFragmentErrors(filterType) { 10740 if (filterType === PlaylistLevelType.AUDIO) { 10741 // Reset current fragment since audio track audio is essential and may not have a fail-over track 10742 this.fragCurrent = null; 10743 } 10744 // Fragment errors that result in a level switch or redundant fail-over 10745 // should reset the stream controller state to idle 10746 if (!this.loadedmetadata) { 10747 this.startFragRequested = false; 10748 } 10749 if (this.state !== State.STOPPED) { 10750 this.state = State.IDLE; 10751 } 10752 } 10753 afterBufferFlushed(media, bufferType, playlistType) { 10754 if (!media) { 10755 return; 10756 } 10757 // After successful buffer flushing, filter flushed fragments from bufferedFrags use mediaBuffered instead of media 10758 // (so that we will check against video.buffered ranges in case of alt audio track) 10759 const bufferedTimeRanges = BufferHelper.getBuffered(media); 10760 this.fragmentTracker.detectEvictedFragments(bufferType, bufferedTimeRanges, playlistType); 10761 if (this.state === State.ENDED) { 10762 this.resetLoadingState(); 10763 } 10764 } 10765 resetLoadingState() { 10766 this.log('Reset loading state'); 10767 this.fragCurrent = null; 10768 this.fragPrevious = null; 10769 this.state = State.IDLE; 10770 } 10771 resetStartWhenNotLoaded(level) { 10772 // if loadedmetadata is not set, it means that first frag request failed 10773 // in that case, reset startFragRequested flag 10774 if (!this.loadedmetadata) { 10775 this.startFragRequested = false; 10776 const details = level ? level.details : null; 10777 if (details != null && details.live) { 10778 // Update the start position and return to IDLE to recover live start 10779 this.startPosition = -1; 10780 this.setStartPosition(details, 0); 10781 this.resetLoadingState(); 10782 } else { 10783 this.nextLoadPosition = this.startPosition; 10784 } 10785 } 10786 }
10787 resetWhenMissingContext(chunkMeta) { 10788 this.warn(`The loading context changed while buffering fragment ${chunkMeta.sn} of level ${chunkMeta.level}. This chunk will not be buffered.`); 10789 this.removeUnbufferedFrags(); 10790 this.resetStartWhenNotLoaded(this.levelLastLoaded); 10791 this.resetLoadingState(); 10792 } 10793 removeUnbufferedFrags(start = 0) { 10794 this.fragmentTracker.removeFragmentsInRange(start, Infinity, this.playlistType, false, true); 10795 } 10796 updateLevelTiming(frag, part, level, partial) { 10797 var _this$transmuxer; 10798 const details = level.details; 10799 if (!details) { 10800 this.warn('level.details undefined'); 10801 return; 10802 } 10803 const parsed = Object.keys(frag.elementaryStreams).reduce((result, type) => { 10804 const info = frag.elementaryStreams[type]; 10805 if (info) { 10806 const parsedDuration = info.endPTS - info.startPTS; 10807 if (parsedDuration <= 0) { 10808 // Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0. 10809 // The new transmuxer will be configured with a time offset matching the next fragment start, 10810 // preventing the timeline from shifting. 10811 this.warn(`Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration})`); 10812 return result || false; 10813 } 10814 const drift = partial ? 0 : updateFragPTSDTS(details, frag, info.startPTS, info.endPTS, info.startDTS, info.endDTS); 10815 this.hls.trigger(Events.LEVEL_PTS_UPDATED, { 10816 details, 10817 level, 10818 drift, 10819 type, 10820 frag, 10821 start: info.startPTS, 10822 end: info.endPTS 10823 }); 10824 return true; 10825 } 10826 return result; 10827 }, false); 10828 if (!parsed && ((_this$transmuxer = this.transmuxer) == null ? void 0 : _this$transmuxer.error) === null) { 10829 const error = new Error(`Found no media in fragment ${frag.sn} of level ${frag.level} resetting transmuxer to fallback to playlist timing`); 10830 if (level.fragmentError === 0) { 10831 // Mark and track the odd empty segment as a gap to avoid reloading 10832 level.fragmentError++; 10833 frag.gap = true; 10834 this.fragmentTracker.removeFragment(frag); 10835 this.fragmentTracker.fragBuffered(frag, true); 10836 } 10837 this.warn(error.message); 10838 this.hls.trigger(Events.ERROR, { 10839 type: ErrorTypes.MEDIA_ERROR, 10840 details: ErrorDetails.FRAG_PARSING_ERROR, 10841 fatal: false, 10842 error, 10843 frag, 10844 reason: `Found no media in msn ${frag.sn} of level "${level.url}"` 10845 }); 10846 if (!this.hls) { 10847 return; 10848 } 10849 this.resetTransmuxer(); 10850 // For this error fallthrough. Marking parsed will allow advancing to next fragment. 10851 } 10852 this.state = State.PARSED; 10853 this.hls.trigger(Events.FRAG_PARSED, { 10854 frag, 10855 part 10856 }); 10857 } 10858 resetTransmuxer() { 10859 if (this.transmuxer) { 10860 this.transmuxer.destroy(); 10861 this.transmuxer = null; 10862 } 10863 } 10864 recoverWorkerError(data) { 10865 if (data.event === 'demuxerWorker') { 10866 this.fragmentTracker.removeAllFragments(); 10867 this.resetTransmuxer(); 10868 this.resetStartWhenNotLoaded(this.levelLastLoaded); 10869 this.resetLoadingState(); 10870 } 10871 } 10872 set state(nextState) { 10873 const previousState = this._state; 10874 if (previousState !== nextState) { 10875 this._state = nextState; 10876 this.log(`${previousState}->${nextState}`); 10877 } 10878 } 10879 get state() { 10880 return this._state; 10881 } 10882} 10883 10884class ChunkCache { 10885 constructor() { 10886 this.chunks = []; 10887 this.dataLength = 0; 10888 } 10889 push(chunk) { 10890 this.chunks.push(chunk); 10891 this.dataLength += chunk.length; 10892 } 10893 flush() { 10894 const { 10895 chunks, 10896 dataLength 10897 } = this; 10898 let result; 10899 if (!chunks.length) { 10900 return new Uint8Array(0); 10901 } else if (chunks.length === 1) { 10902 result = chunks[0]; 10903 } else { 10904 result = concatUint8Arrays(chunks, dataLength); 10905 } 10906 this.reset(); 10907 return result; 10908 } 10909 reset() { 10910 this.chunks.length = 0; 10911 this.dataLength = 0; 10912 } 10913} 10914function concatUint8Arrays(chunks, dataLength) { 10915 const result = new Uint8Array(dataLength); 10916 let offset = 0; 10917 for (let i = 0; i < chunks.length; i++) { 10918 const chunk = chunks[i]; 10919 result.set(chunk, offset); 10920 offset += chunk.length; 10921 } 10922 return result; 10923} 10924 10925// ensure the worker ends up in the bundle 10926// If the worker should not be included this gets aliased to empty.js 10927function hasUMDWorker() { 10928 return typeof __HLS_WORKER_BUNDLE__ === 'function'; 10929} 10930function injectWorker() { 10931 const blob = new self.Blob([`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`], {
10932 type: 'text/javascript' 10933 }); 10934 const objectURL = self.URL.createObjectURL(blob); 10935 const worker = new self.Worker(objectURL); 10936 return { 10937 worker, 10938 objectURL 10939 }; 10940} 10941function loadWorker(path) { 10942 const scriptURL = new self.URL(path, self.location.href).href; 10943 const worker = new self.Worker(scriptURL); 10944 return { 10945 worker, 10946 scriptURL 10947 }; 10948} 10949 10950function dummyTrack(type = '', inputTimeScale = 90000) { 10951 return { 10952 type, 10953 id: -1, 10954 pid: -1, 10955 inputTimeScale, 10956 sequenceNumber: -1, 10957 samples: [], 10958 dropped: 0 10959 }; 10960} 10961 10962class BaseAudioDemuxer { 10963 constructor() { 10964 this._audioTrack = void 0; 10965 this._id3Track = void 0; 10966 this.frameIndex = 0; 10967 this.cachedData = null; 10968 this.basePTS = null; 10969 this.initPTS = null; 10970 this.lastPTS = null; 10971 } 10972 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 10973 this._id3Track = { 10974 type: 'id3', 10975 id: 3, 10976 pid: -1, 10977 inputTimeScale: 90000, 10978 sequenceNumber: 0, 10979 samples: [], 10980 dropped: 0 10981 }; 10982 } 10983 resetTimeStamp(deaultTimestamp) { 10984 this.initPTS = deaultTimestamp; 10985 this.resetContiguity(); 10986 } 10987 resetContiguity() { 10988 this.basePTS = null; 10989 this.lastPTS = null; 10990 this.frameIndex = 0; 10991 } 10992 canParse(data, offset) { 10993 return false; 10994 } 10995 appendFrame(track, data, offset) {} 10996 10997 // feed incoming data to the front of the parsing pipeline 10998 demux(data, timeOffset) { 10999 if (this.cachedData) { 11000 data = appendUint8Array(this.cachedData, data); 11001 this.cachedData = null; 11002 } 11003 let id3Data = getID3Data(data, 0); 11004 let offset = id3Data ? id3Data.length : 0; 11005 let lastDataIndex; 11006 const track = this._audioTrack; 11007 const id3Track = this._id3Track; 11008 const timestamp = id3Data ? getTimeStamp(id3Data) : undefined; 11009 const length = data.length; 11010 if (this.basePTS === null || this.frameIndex === 0 && isFiniteNumber(timestamp)) { 11011 this.basePTS = initPTSFn(timestamp, timeOffset, this.initPTS); 11012 this.lastPTS = this.basePTS; 11013 } 11014 if (this.lastPTS === null) { 11015 this.lastPTS = this.basePTS; 11016 } 11017 11018 // more expressive than alternative: id3Data?.length 11019 if (id3Data && id3Data.length > 0) { 11020 id3Track.samples.push({ 11021 pts: this.lastPTS, 11022 dts: this.lastPTS, 11023 data: id3Data, 11024 type: MetadataSchema.audioId3, 11025 duration: Number.POSITIVE_INFINITY 11026 }); 11027 } 11028 while (offset < length) { 11029 if (this.canParse(data, offset)) { 11030 const frame = this.appendFrame(track, data, offset); 11031 if (frame) { 11032 this.frameIndex++; 11033 this.lastPTS = frame.sample.pts; 11034 offset += frame.length; 11035 lastDataIndex = offset; 11036 } else { 11037 offset = length; 11038 } 11039 } else if (canParse$2(data, offset)) { 11040 // after a ID3.canParse, a call to ID3.getID3Data *should* always returns some data 11041 id3Data = getID3Data(data, offset); 11042 id3Track.samples.push({ 11043 pts: this.lastPTS, 11044 dts: this.lastPTS, 11045 data: id3Data, 11046 type: MetadataSchema.audioId3, 11047 duration: Number.POSITIVE_INFINITY 11048 }); 11049 offset += id3Data.length; 11050 lastDataIndex = offset; 11051 } else { 11052 offset++; 11053 } 11054 if (offset === length && lastDataIndex !== length) { 11055 const partialData = sliceUint8(data, lastDataIndex); 11056 if (this.cachedData) { 11057 this.cachedData = appendUint8Array(this.cachedData, partialData); 11058 } else { 11059 this.cachedData = partialData; 11060 } 11061 } 11062 } 11063 return { 11064 audioTrack: track, 11065 videoTrack: dummyTrack(), 11066 id3Track, 11067 textTrack: dummyTrack() 11068 }; 11069 } 11070 demuxSampleAes(data, keyData, timeOffset) { 11071 return Promise.reject(new Error(`[${this}] This demuxer does not support Sample-AES decryption`)); 11072 } 11073 flush(timeOffset) { 11074 // Parse cache in case of remaining frames. 11075 const cachedData = this.cachedData; 11076 if (cachedData) { 11077 this.cachedData = null; 11078 this.demux(cachedData, 0); 11079 } 11080 return { 11081 audioTrack: this._audioTrack, 11082 videoTrack: dummyTrack(), 11083 id3Track: this._id3Track, 11084 textTrack: dummyTrack() 11085 }; 11086 } 11087 destroy() {} 11088} 11089 11090/** 11091 * Initialize PTS 11092 * <p> 11093 * use timestamp unless it is undefined, NaN or Infinity 11094 * </p> 11095 */ 11096const initPTSFn = (timestamp, timeOffset, initPTS) => { 11097 if (isFiniteNumber(timestamp)) { 11098 return timestamp * 90; 11099 } 11100 const init90kHz = initPTS ? initPTS.baseTime * 90000 / initPTS.timescale : 0; 11101 return timeOffset * 90000 + init90kHz; 11102}; 11103 11104/** 11105 * ADTS parser helper 11106 * @link https://wiki.multimedia.cx/index.php?title=ADTS 11107 */ 11108function getAudioConfig(observer, data, offset, audioCodec) { 11109 let adtsObjectType; 11110 let adtsExtensionSamplingIndex; 11111 let adtsChannelConfig; 11112 let config; 11113 const userAgent = navigator.userAgent.toLowerCase(); 11114 const manifestCodec = audioCodec; 11115 const adtsSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 11116 // byte 2 11117 adtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1; 11118 const adtsSamplingIndex = (data[offset + 2] & 0x3c) >>> 2; 11119 if (adtsSamplingIndex > adtsSamplingRates.length - 1) { 11120 const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`); 11121 observer.emit(Events.ERROR, Events.ERROR, { 11122 type: ErrorTypes.MEDIA_ERROR, 11123 details: ErrorDetails.FRAG_PARSING_ERROR, 11124 fatal: true, 11125 error, 11126 reason: error.message 11127 }); 11128 return; 11129 } 11130 adtsChannelConfig = (data[offset + 2] & 0x01) << 2; 11131 // byte 3 11132 adtsChannelConfig |= (data[offset + 3] & 0xc0) >>> 6; 11133 logger.log(`manifest codec:${audioCodec}, ADTS type:${adtsObjectType}, samplingIndex:${adtsSamplingIndex}`); 11134 // firefox: freq less than 24kHz = AAC SBR (HE-AAC) 11135 if (/firefox/i.test(userAgent)) { 11136 if (adtsSamplingIndex >= 6) { 11137 adtsObjectType = 5; 11138 config = new Array(4); 11139 // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies 11140 // there is a factor 2 between frame sample rate and output sample rate 11141 // multiply frequency by 2 (see table below, equivalent to substract 3) 11142 adtsExtensionSamplingIndex = adtsSamplingIndex - 3; 11143 } else { 11144 adtsObjectType = 2; 11145 config = new Array(2); 11146 adtsExtensionSamplingIndex = adtsSamplingIndex; 11147 } 11148 // Android : always use AAC 11149 } else if (userAgent.indexOf('android') !== -1) { 11150 adtsObjectType = 2; 11151 config = new Array(2); 11152 adtsExtensionSamplingIndex = adtsSamplingIndex; 11153 } else { 11154 /* for other browsers (Chrome/Vivaldi/Opera ...) 11155 always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...) 11156 */ 11157 adtsObjectType = 5; 11158 config = new Array(4); 11159 // if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz) 11160 if (audioCodec && (audioCodec.indexOf('mp4a.40.29') !== -1 || audioCodec.indexOf('mp4a.40.5') !== -1) || !audioCodec && adtsSamplingIndex >= 6) { 11161 // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies 11162 // there is a factor 2 between frame sample rate and output sample rate 11163 // multiply frequency by 2 (see table below, equivalent to substract 3) 11164 adtsExtensionSamplingIndex = adtsSamplingIndex - 3; 11165 } else { 11166 // if (manifest codec is AAC) AND (frequency less than 24kHz AND nb channel is 1) OR (manifest codec not specified and mono audio) 11167 // Chrome fails to play back with low frequency AAC LC mono when initialized with HE-AAC. This is not a problem with stereo. 11168 if (audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && (adtsSamplingIndex >= 6 && adtsChannelConfig === 1 || /vivaldi/i.test(userAgent)) || !audioCodec && adtsChannelConfig === 1) { 11169 adtsObjectType = 2; 11170 config = new Array(2); 11171 } 11172 adtsExtensionSamplingIndex = adtsSamplingIndex; 11173 } 11174 } 11175 /* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config 11176 ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig() 11177 Audio Profile / Audio Object Type 11178 0: Null 11179 1: AAC Main 11180 2: AAC LC (Low Complexity) 11181 3: AAC SSR (Scalable Sample Rate) 11182 4: AAC LTP (Long Term Prediction) 11183 5: SBR (Spectral Band Replication) 11184 6: AAC Scalable 11185 sampling freq 11186 0: 96000 Hz 11187 1: 88200 Hz 11188 2: 64000 Hz 11189 3: 48000 Hz 11190 4: 44100 Hz 11191 5: 32000 Hz 11192 6: 24000 Hz 11193 7: 22050 Hz 11194 8: 16000 Hz 11195 9: 12000 Hz 11196 10: 11025 Hz 11197 11: 8000 Hz 11198 12: 7350 Hz 11199 13: Reserved 11200 14: Reserved 11201 15: frequency is written explictly 11202 Channel Configurations 11203 These are the channel configurations: 11204 0: Defined in AOT Specifc Config 11205 1: 1 channel: front-center 11206 2: 2 channels: front-left, front-right 11207 */ 11208 // audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1 11209 config[0] = adtsObjectType << 3; 11210 // samplingFrequencyIndex 11211 config[0] |= (adtsSamplingIndex & 0x0e) >> 1; 11212 config[1] |= (adtsSamplingIndex & 0x01) << 7; 11213 // channelConfiguration 11214 config[1] |= adtsChannelConfig << 3; 11215 if (adtsObjectType === 5) { 11216 // adtsExtensionSamplingIndex 11217 config[1] |= (adtsExtensionSamplingIndex & 0x0e) >> 1; 11218 config[2] = (adtsExtensionSamplingIndex & 0x01) << 7; 11219 // adtsObjectType (force to 2, chrome is checking that object type is less than 5 ??? 11220 // https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc 11221 config[2] |= 2 << 2; 11222 config[3] = 0; 11223 } 11224 return { 11225 config, 11226 samplerate: adtsSamplingRates[adtsSamplingIndex], 11227 channelCount: adtsChannelConfig, 11228 codec: 'mp4a.40.' + adtsObjectType, 11229 manifestCodec 11230 }; 11231} 11232function isHeaderPattern$1(data, offset) { 11233 return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0; 11234} 11235function getHeaderLength(data, offset) { 11236 return data[offset + 1] & 0x01 ? 7 : 9; 11237} 11238function getFullFrameLength(data, offset) { 11239 return (data[offset + 3] & 0x03) << 11 | data[offset + 4] << 3 | (data[offset + 5] & 0xe0) >>> 5; 11240} 11241function canGetFrameLength(data, offset) { 11242 return offset + 5 < data.length; 11243} 11244function isHeader$1(data, offset) { 11245 // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 11246 // Layer bits (position 14 and 15) in header should be always 0 for ADTS 11247 // More info https://wiki.multimedia.cx/index.php?title=ADTS 11248 return offset + 1 < data.length && isHeaderPattern$1(data, offset); 11249} 11250function canParse$1(data, offset) { 11251 return canGetFrameLength(data, offset) && isHeaderPattern$1(data, offset) && getFullFrameLength(data, offset) <= data.length - offset; 11252} 11253function probe$1(data, offset) { 11254 // same as isHeader but we also check that ADTS frame follows last ADTS frame 11255 // or end of data is reached 11256 if (isHeader$1(data, offset)) { 11257 // ADTS header Length 11258 const headerLength = getHeaderLength(data, offset); 11259 if (offset + headerLength >= data.length) { 11260 return false;
vendor: 4,485 bytes, lines 11261-11429
11261 } 11262 // ADTS frame Length 11263 const frameLength = getFullFrameLength(data, offset); 11264 if (frameLength <= headerLength) { 11265 return false; 11266 } 11267 const newOffset = offset + frameLength; 11268 return newOffset === data.length || isHeader$1(data, newOffset); 11269 } 11270 return false; 11271} 11272function initTrackConfig(track, observer, data, offset, audioCodec) { 11273 if (!track.samplerate) { 11274 const config = getAudioConfig(observer, data, offset, audioCodec); 11275 if (!config) { 11276 return; 11277 } 11278 track.config = config.config; 11279 track.samplerate = config.samplerate; 11280 track.channelCount = config.channelCount; 11281 track.codec = config.codec; 11282 track.manifestCodec = config.manifestCodec; 11283 logger.log(`parsed codec:${track.codec}, rate:${config.samplerate}, channels:${config.channelCount}`); 11284 } 11285} 11286function getFrameDuration(samplerate) { 11287 return 1024 * 90000 / samplerate; 11288} 11289function parseFrameHeader(data, offset) { 11290 // The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header 11291 const headerLength = getHeaderLength(data, offset); 11292 if (offset + headerLength <= data.length) { 11293 // retrieve frame size 11294 const frameLength = getFullFrameLength(data, offset) - headerLength; 11295 if (frameLength > 0) { 11296 // logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}`); 11297 return { 11298 headerLength, 11299 frameLength 11300 }; 11301 } 11302 } 11303} 11304function appendFrame$2(track, data, offset, pts, frameIndex) { 11305 const frameDuration = getFrameDuration(track.samplerate); 11306 const stamp = pts + frameIndex * frameDuration; 11307 const header = parseFrameHeader(data, offset); 11308 let unit; 11309 if (header) { 11310 const { 11311 frameLength, 11312 headerLength 11313 } = header; 11314 const _length = headerLength + frameLength; 11315 const missing = Math.max(0, offset + _length - data.length); 11316 // logger.log(`AAC frame ${frameIndex}, pts:${stamp} length@offset/total: ${frameLength}@${offset+headerLength}/${data.byteLength} missing: ${missing}`); 11317 if (missing) { 11318 unit = new Uint8Array(_length - headerLength); 11319 unit.set(data.subarray(offset + headerLength, data.length), 0); 11320 } else { 11321 unit = data.subarray(offset + headerLength, offset + _length); 11322 } 11323 const _sample = { 11324 unit, 11325 pts: stamp 11326 }; 11327 if (!missing) { 11328 track.samples.push(_sample); 11329 } 11330 return { 11331 sample: _sample, 11332 length: _length, 11333 missing 11334 }; 11335 } 11336 // overflow incomplete header 11337 const length = data.length - offset; 11338 unit = new Uint8Array(length); 11339 unit.set(data.subarray(offset, data.length), 0); 11340 const sample = { 11341 unit, 11342 pts: stamp 11343 }; 11344 return { 11345 sample, 11346 length, 11347 missing: -1 11348 }; 11349} 11350 11351/** 11352 * MPEG parser helper 11353 */ 11354 11355let chromeVersion$1 = null; 11356const BitratesMap = [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]; 11357const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000]; 11358const SamplesCoefficients = [ 11359// MPEG 2.5 11360[0, 11361// Reserved 1136272, 11363// Layer3 11364144, 11365// Layer2 1136612 // Layer1 11367], 11368// Reserved 11369[0, 11370// Reserved 113710, 11372// Layer3 113730, 11374// Layer2 113750 // Layer1 11376], 11377// MPEG 2 11378[0, 11379// Reserved 1138072, 11381// Layer3 11382144, 11383// Layer2 1138412 // Layer1 11385], 11386// MPEG 1 11387[0, 11388// Reserved 11389144, 11390// Layer3 11391144, 11392// Layer2 1139312 // Layer1 11394]]; 11395const BytesInSlot = [0, 11396// Reserved 113971, 11398// Layer3 113991, 11400// Layer2 114014 // Layer1 11402]; 11403function appendFrame$1(track, data, offset, pts, frameIndex) { 11404 // Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference 11405 if (offset + 24 > data.length) { 11406 return; 11407 } 11408 const header = parseHeader(data, offset); 11409 if (header && offset + header.frameLength <= data.length) { 11410 const frameDuration = header.samplesPerFrame * 90000 / header.sampleRate; 11411 const stamp = pts + frameIndex * frameDuration; 11412 const sample = { 11413 unit: data.subarray(offset, offset + header.frameLength), 11414 pts: stamp, 11415 dts: stamp 11416 }; 11417 track.config = []; 11418 track.channelCount = header.channelCount; 11419 track.samplerate = header.sampleRate; 11420 track.samples.push(sample); 11421 return { 11422 sample, 11423 length: header.frameLength, 11424 missing: 0 11425 }; 11426 } 11427} 11428function parseHeader(data, offset) { 11429 const mpegVersion = data[offset + 1] >> 3 & 3;
vendor: 4,249 bytes, lines 11430-11535
11430 const mpegLayer = data[offset + 1] >> 1 & 3; 11431 const bitRateIndex = data[offset + 2] >> 4 & 15; 11432 const sampleRateIndex = data[offset + 2] >> 2 & 3; 11433 if (mpegVersion !== 1 && bitRateIndex !== 0 && bitRateIndex !== 15 && sampleRateIndex !== 3) { 11434 const paddingBit = data[offset + 2] >> 1 & 1; 11435 const channelMode = data[offset + 3] >> 6; 11436 const columnInBitrates = mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4; 11437 const bitRate = BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000; 11438 const columnInSampleRates = mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2; 11439 const sampleRate = SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex]; 11440 const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono) 11441 const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer]; 11442 const bytesInSlot = BytesInSlot[mpegLayer]; 11443 const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot; 11444 const frameLength = Math.floor(sampleCoefficient * bitRate / sampleRate + paddingBit) * bytesInSlot; 11445 if (chromeVersion$1 === null) { 11446 const userAgent = navigator.userAgent || ''; 11447 const result = userAgent.match(/Chrome\/(\d+)/i); 11448 chromeVersion$1 = result ? parseInt(result[1]) : 0; 11449 } 11450 const needChromeFix = !!chromeVersion$1 && chromeVersion$1 <= 87; 11451 if (needChromeFix && mpegLayer === 2 && bitRate >= 224000 && channelMode === 0) { 11452 // Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00) 11453 data[offset + 3] = data[offset + 3] | 0x80; 11454 } 11455 return { 11456 sampleRate, 11457 channelCount, 11458 frameLength, 11459 samplesPerFrame 11460 }; 11461 } 11462} 11463function isHeaderPattern(data, offset) { 11464 return data[offset] === 0xff && (data[offset + 1] & 0xe0) === 0xe0 && (data[offset + 1] & 0x06) !== 0x00; 11465} 11466function isHeader(data, offset) { 11467 // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 11468 // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) 11469 // More info http://www.mp3-tech.org/programmer/frame_header.html 11470 return offset + 1 < data.length && isHeaderPattern(data, offset); 11471} 11472function canParse(data, offset) { 11473 const headerSize = 4; 11474 return isHeaderPattern(data, offset) && headerSize <= data.length - offset; 11475} 11476function probe(data, offset) { 11477 // same as isHeader but we also check that MPEG frame follows last MPEG frame 11478 // or end of data is reached 11479 if (offset + 1 < data.length && isHeaderPattern(data, offset)) { 11480 // MPEG header Length 11481 const headerLength = 4; 11482 // MPEG frame Length 11483 const header = parseHeader(data, offset); 11484 let frameLength = headerLength; 11485 if (header != null && header.frameLength) { 11486 frameLength = header.frameLength; 11487 } 11488 const newOffset = offset + frameLength; 11489 return newOffset === data.length || isHeader(data, newOffset); 11490 } 11491 return false; 11492} 11493 11494/** 11495 * AAC demuxer 11496 */ 11497class AACDemuxer extends BaseAudioDemuxer { 11498 constructor(observer, config) { 11499 super(); 11500 this.observer = void 0; 11501 this.config = void 0; 11502 this.observer = observer; 11503 this.config = config; 11504 } 11505 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11506 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 11507 this._audioTrack = { 11508 container: 'audio/adts', 11509 type: 'audio', 11510 id: 2, 11511 pid: -1, 11512 sequenceNumber: 0, 11513 segmentCodec: 'aac', 11514 samples: [], 11515 manifestCodec: audioCodec, 11516 duration: trackDuration, 11517 inputTimeScale: 90000, 11518 dropped: 0 11519 }; 11520 } 11521 11522 // Source for probe info - https://wiki.multimedia.cx/index.php?title=ADTS 11523 static probe(data) { 11524 if (!data) { 11525 return false; 11526 } 11527 11528 // Check for the ADTS sync word 11529 // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 11530 // Layer bits (position 14 and 15) in header should be always 0 for ADTS 11531 // More info https://wiki.multimedia.cx/index.php?title=ADTS 11532 const id3Data = getID3Data(data, 0); 11533 let offset = (id3Data == null ? void 0 : id3Data.length) || 0; 11534 if (probe(data, offset)) { 11535 return false;
vendor: 8,286 bytes, lines 11536-11783
11536 } 11537 for (let length = data.length; offset < length; offset++) { 11538 if (probe$1(data, offset)) { 11539 logger.log('ADTS sync word found !'); 11540 return true; 11541 } 11542 } 11543 return false; 11544 } 11545 canParse(data, offset) { 11546 return canParse$1(data, offset); 11547 } 11548 appendFrame(track, data, offset) { 11549 initTrackConfig(track, this.observer, data, offset, track.manifestCodec); 11550 const frame = appendFrame$2(track, data, offset, this.basePTS, this.frameIndex); 11551 if (frame && frame.missing === 0) { 11552 return frame; 11553 } 11554 } 11555} 11556 11557const emsgSchemePattern = /\/emsg[-/]ID3/i; 11558class MP4Demuxer { 11559 constructor(observer, config) { 11560 this.remainderData = null; 11561 this.timeOffset = 0; 11562 this.config = void 0; 11563 this.videoTrack = void 0; 11564 this.audioTrack = void 0; 11565 this.id3Track = void 0; 11566 this.txtTrack = void 0; 11567 this.config = config; 11568 } 11569 resetTimeStamp() {} 11570 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11571 const videoTrack = this.videoTrack = dummyTrack('video', 1); 11572 const audioTrack = this.audioTrack = dummyTrack('audio', 1); 11573 const captionTrack = this.txtTrack = dummyTrack('text', 1); 11574 this.id3Track = dummyTrack('id3', 1); 11575 this.timeOffset = 0; 11576 if (!(initSegment != null && initSegment.byteLength)) { 11577 return; 11578 } 11579 const initData = parseInitSegment(initSegment); 11580 if (initData.video) { 11581 const { 11582 id, 11583 timescale, 11584 codec 11585 } = initData.video; 11586 videoTrack.id = id; 11587 videoTrack.timescale = captionTrack.timescale = timescale; 11588 videoTrack.codec = codec; 11589 } 11590 if (initData.audio) { 11591 const { 11592 id, 11593 timescale, 11594 codec 11595 } = initData.audio; 11596 audioTrack.id = id; 11597 audioTrack.timescale = timescale; 11598 audioTrack.codec = codec; 11599 } 11600 captionTrack.id = RemuxerTrackIdConfig.text; 11601 videoTrack.sampleDuration = 0; 11602 videoTrack.duration = audioTrack.duration = trackDuration; 11603 } 11604 resetContiguity() { 11605 this.remainderData = null; 11606 } 11607 static probe(data) { 11608 return hasMoofData(data); 11609 } 11610 demux(data, timeOffset) { 11611 this.timeOffset = timeOffset; 11612 // Load all data into the avc track. The CMAF remuxer will look for the data in the samples object; the rest of the fields do not matter 11613 let videoSamples = data; 11614 const videoTrack = this.videoTrack; 11615 const textTrack = this.txtTrack; 11616 if (this.config.progressive) { 11617 // Split the bytestream into two ranges: one encompassing all data up until the start of the last moof, and everything else. 11618 // This is done to guarantee that we're sending valid data to MSE - when demuxing progressively, we have no guarantee 11619 // that the fetch loader gives us flush moof+mdat pairs. If we push jagged data to MSE, it will throw an exception. 11620 if (this.remainderData) { 11621 videoSamples = appendUint8Array(this.remainderData, data); 11622 } 11623 const segmentedData = segmentValidRange(videoSamples); 11624 this.remainderData = segmentedData.remainder; 11625 videoTrack.samples = segmentedData.valid || new Uint8Array(); 11626 } else { 11627 videoTrack.samples = videoSamples; 11628 } 11629 const id3Track = this.extractID3Track(videoTrack, timeOffset); 11630 textTrack.samples = parseSamples(timeOffset, videoTrack); 11631 return { 11632 videoTrack, 11633 audioTrack: this.audioTrack, 11634 id3Track, 11635 textTrack: this.txtTrack 11636 }; 11637 } 11638 flush() { 11639 const timeOffset = this.timeOffset; 11640 const videoTrack = this.videoTrack; 11641 const textTrack = this.txtTrack; 11642 videoTrack.samples = this.remainderData || new Uint8Array(); 11643 this.remainderData = null; 11644 const id3Track = this.extractID3Track(videoTrack, this.timeOffset); 11645 textTrack.samples = parseSamples(timeOffset, videoTrack); 11646 return { 11647 videoTrack, 11648 audioTrack: dummyTrack(), 11649 id3Track, 11650 textTrack: dummyTrack() 11651 }; 11652 } 11653 extractID3Track(videoTrack, timeOffset) { 11654 const id3Track = this.id3Track; 11655 if (videoTrack.samples.length) { 11656 const emsgs = findBox(videoTrack.samples, ['emsg']); 11657 if (emsgs) { 11658 emsgs.forEach(data => { 11659 const emsgInfo = parseEmsg(data); 11660 if (emsgSchemePattern.test(emsgInfo.schemeIdUri)) { 11661 const pts = isFiniteNumber(emsgInfo.presentationTime) ? emsgInfo.presentationTime / emsgInfo.timeScale : timeOffset + emsgInfo.presentationTimeDelta / emsgInfo.timeScale; 11662 let duration = emsgInfo.eventDuration === 0xffffffff ? Number.POSITIVE_INFINITY : emsgInfo.eventDuration / emsgInfo.timeScale; 11663 // Safari takes anything <= 0.001 seconds and maps it to Infinity 11664 if (duration <= 0.001) { 11665 duration = Number.POSITIVE_INFINITY; 11666 } 11667 const payload = emsgInfo.payload; 11668 id3Track.samples.push({ 11669 data: payload, 11670 len: payload.byteLength, 11671 dts: pts, 11672 pts: pts, 11673 type: MetadataSchema.emsg, 11674 duration: duration 11675 }); 11676 } 11677 }); 11678 } 11679 } 11680 return id3Track; 11681 } 11682 demuxSampleAes(data, keyData, timeOffset) { 11683 return Promise.reject(new Error('The MP4 demuxer does not support SAMPLE-AES decryption')); 11684 } 11685 destroy() {} 11686} 11687 11688const getAudioBSID = (data, offset) => { 11689 // check the bsid to confirm ac-3 | ec-3 11690 let bsid = 0; 11691 let numBits = 5; 11692 offset += numBits; 11693 const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage 11694 const mask = new Uint32Array(1); // unsigned 32 bit mask value 11695 const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage 11696 while (numBits > 0) { 11697 byte[0] = data[offset]; 11698 // read remaining bits, upto 8 bits at a time 11699 const bits = Math.min(numBits, 8); 11700 const shift = 8 - bits; 11701 mask[0] = 0xff000000 >>> 24 + shift << shift; 11702 temp[0] = (byte[0] & mask[0]) >> shift; 11703 bsid = !bsid ? temp[0] : bsid << bits | temp[0]; 11704 offset += 1; 11705 numBits -= bits; 11706 } 11707 return bsid; 11708}; 11709 11710class AC3Demuxer extends BaseAudioDemuxer { 11711 constructor(observer) { 11712 super(); 11713 this.observer = void 0; 11714 this.observer = observer; 11715 } 11716 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11717 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 11718 this._audioTrack = { 11719 container: 'audio/ac-3', 11720 type: 'audio', 11721 id: 2, 11722 pid: -1, 11723 sequenceNumber: 0, 11724 segmentCodec: 'ac3', 11725 samples: [], 11726 manifestCodec: audioCodec, 11727 duration: trackDuration, 11728 inputTimeScale: 90000, 11729 dropped: 0 11730 }; 11731 } 11732 canParse(data, offset) { 11733 return offset + 64 < data.length; 11734 } 11735 appendFrame(track, data, offset) { 11736 const frameLength = appendFrame(track, data, offset, this.basePTS, this.frameIndex); 11737 if (frameLength !== -1) { 11738 const sample = track.samples[track.samples.length - 1]; 11739 return { 11740 sample, 11741 length: frameLength, 11742 missing: 0 11743 }; 11744 } 11745 } 11746 static probe(data) { 11747 if (!data) { 11748 return false; 11749 } 11750 const id3Data = getID3Data(data, 0); 11751 if (!id3Data) { 11752 return false; 11753 } 11754 11755 // look for the ac-3 sync bytes 11756 const offset = id3Data.length; 11757 if (data[offset] === 0x0b && data[offset + 1] === 0x77 && getTimeStamp(id3Data) !== undefined && 11758 // check the bsid to confirm ac-3 11759 getAudioBSID(data, offset) < 16) { 11760 return true; 11761 } 11762 return false; 11763 } 11764} 11765function appendFrame(track, data, start, pts, frameIndex) { 11766 if (start + 8 > data.length) { 11767 return -1; // not enough bytes left 11768 } 11769 if (data[start] !== 0x0b || data[start + 1] !== 0x77) { 11770 return -1; // invalid magic 11771 } 11772 11773 // get sample rate 11774 const samplingRateCode = data[start + 4] >> 6; 11775 if (samplingRateCode >= 3) { 11776 return -1; // invalid sampling rate 11777 } 11778 const samplingRateMap = [48000, 44100, 32000]; 11779 const sampleRate = samplingRateMap[samplingRateCode]; 11780 11781 // get frame size 11782 const frameSizeCode = data[start + 4] & 0x3f; 11783 const frameSizeMap = [64, 69, 96, 64, 70, 96, 80, 87, 120, 80, 88, 120, 96, 104, 144, 96, 105, 144, 112, 121, 168, 112, 122, 168, 128, 139, 192, 128, 140, 192, 160, 174, 240, 160, 175, 240, 192, 208, 288, 192, 209, 288, 224, 243, 336, 224, 244, 336, 256, 278, 384, 256, 279, 384, 320, 348, 480, 320, 349, 480, 384, 417, 576, 384, 418, 5
vendor: 5,407 bytes, lines 11783-11956
1178376, 448, 487, 672, 448, 488, 672, 512, 557, 768, 512, 558, 768, 640, 696, 960, 640, 697, 960, 768, 835, 1152, 768, 836, 1152, 896, 975, 1344, 896, 976, 1344, 1024, 1114, 1536, 1024, 1115, 1536, 1152, 1253, 1728, 1152, 1254, 1728, 1280, 1393, 1920, 1280, 1394, 1920]; 11784 const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2; 11785 if (start + frameLength > data.length) { 11786 return -1; 11787 } 11788 11789 // get channel count 11790 const channelMode = data[start + 6] >> 5; 11791 let skipCount = 0; 11792 if (channelMode === 2) { 11793 skipCount += 2; 11794 } else { 11795 if (channelMode & 1 && channelMode !== 1) { 11796 skipCount += 2; 11797 } 11798 if (channelMode & 4) { 11799 skipCount += 2; 11800 } 11801 } 11802 const lfeon = (data[start + 6] << 8 | data[start + 7]) >> 12 - skipCount & 1; 11803 const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5]; 11804 const channelCount = channelsMap[channelMode] + lfeon; 11805 11806 // build dac3 box 11807 const bsid = data[start + 5] >> 3; 11808 const bsmod = data[start + 5] & 7; 11809 const config = new Uint8Array([samplingRateCode << 6 | bsid << 1 | bsmod >> 2, (bsmod & 3) << 6 | channelMode << 3 | lfeon << 2 | frameSizeCode >> 4, frameSizeCode << 4 & 0xe0]); 11810 const frameDuration = 1536 / sampleRate * 90000; 11811 const stamp = pts + frameIndex * frameDuration; 11812 const unit = data.subarray(start, start + frameLength); 11813 track.config = config; 11814 track.channelCount = channelCount; 11815 track.samplerate = sampleRate; 11816 track.samples.push({ 11817 unit, 11818 pts: stamp 11819 }); 11820 return frameLength; 11821} 11822 11823class BaseVideoParser { 11824 constructor() { 11825 this.VideoSample = null; 11826 } 11827 createVideoSample(key, pts, dts, debug) { 11828 return { 11829 key, 11830 frame: false, 11831 pts, 11832 dts, 11833 units: [], 11834 debug, 11835 length: 0 11836 }; 11837 } 11838 getLastNalUnit(samples) { 11839 var _VideoSample; 11840 let VideoSample = this.VideoSample; 11841 let lastUnit; 11842 // try to fallback to previous sample if current one is empty 11843 if (!VideoSample || VideoSample.units.length === 0) { 11844 VideoSample = samples[samples.length - 1]; 11845 } 11846 if ((_VideoSample = VideoSample) != null && _VideoSample.units) { 11847 const units = VideoSample.units; 11848 lastUnit = units[units.length - 1]; 11849 } 11850 return lastUnit; 11851 } 11852 pushAccessUnit(VideoSample, videoTrack) { 11853 if (VideoSample.units.length && VideoSample.frame) { 11854 // if sample does not have PTS/DTS, patch with last sample PTS/DTS 11855 if (VideoSample.pts === undefined) { 11856 const samples = videoTrack.samples; 11857 const nbSamples = samples.length; 11858 if (nbSamples) { 11859 const lastSample = samples[nbSamples - 1]; 11860 VideoSample.pts = lastSample.pts; 11861 VideoSample.dts = lastSample.dts; 11862 } else { 11863 // dropping samples, no timestamp found 11864 videoTrack.dropped++; 11865 return; 11866 } 11867 } 11868 videoTrack.samples.push(VideoSample); 11869 } 11870 if (VideoSample.debug.length) { 11871 logger.log(VideoSample.pts + '/' + VideoSample.dts + ':' + VideoSample.debug); 11872 } 11873 } 11874} 11875 11876/** 11877 * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. 11878 */ 11879 11880class ExpGolomb { 11881 constructor(data) { 11882 this.data = void 0; 11883 this.bytesAvailable = void 0; 11884 this.word = void 0; 11885 this.bitsAvailable = void 0; 11886 this.data = data; 11887 // the number of bytes left to examine in this.data 11888 this.bytesAvailable = data.byteLength; 11889 // the current word being examined 11890 this.word = 0; // :uint 11891 // the number of bits left to examine in the current word 11892 this.bitsAvailable = 0; // :uint 11893 } 11894 11895 // ():void 11896 loadWord() { 11897 const data = this.data; 11898 const bytesAvailable = this.bytesAvailable; 11899 const position = data.byteLength - bytesAvailable; 11900 const workingBytes = new Uint8Array(4); 11901 const availableBytes = Math.min(4, bytesAvailable); 11902 if (availableBytes === 0) { 11903 throw new Error('no bytes available'); 11904 } 11905 workingBytes.set(data.subarray(position, position + availableBytes)); 11906 this.word = new DataView(workingBytes.buffer).getUint32(0); 11907 // track the amount of this.data that has been processed 11908 this.bitsAvailable = availableBytes * 8; 11909 this.bytesAvailable -= availableBytes; 11910 } 11911 11912 // (count:int):void 11913 skipBits(count) { 11914 let skipBytes; // :int 11915 count = Math.min(count, this.bytesAvailable * 8 + this.bitsAvailable); 11916 if (this.bitsAvailable > count) { 11917 this.word <<= count; 11918 this.bitsAvailable -= count; 11919 } else { 11920 count -= this.bitsAvailable; 11921 skipBytes = count >> 3; 11922 count -= skipBytes << 3; 11923 this.bytesAvailable -= skipBytes; 11924 this.loadWord(); 11925 this.word <<= count; 11926 this.bitsAvailable -= count; 11927 } 11928 } 11929 11930 // (size:int):uint 11931 readBits(size) { 11932 let bits = Math.min(this.bitsAvailable, size); // :uint 11933 const valu = this.word >>> 32 - bits; // :uint 11934 if (size > 32) { 11935 logger.error('Cannot read more than 32 bits at a time'); 11936 } 11937 this.bitsAvailable -= bits; 11938 if (this.bitsAvailable > 0) { 11939 this.word <<= bits; 11940 } else if (this.bytesAvailable > 0) { 11941 this.loadWord(); 11942 } else { 11943 throw new Error('no bits available'); 11944 } 11945 bits = size - bits; 11946 if (bits > 0 && this.bitsAvailable) { 11947 return valu << bits | this.readBits(bits); 11948 } else { 11949 return valu; 11950 } 11951 } 11952 11953 // ():uint 11954 skipLZ() { 11955 let leadingZeroCount; // :uint 11956 for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable;
vendor: 7,497 bytes, lines 11956-12206
11956 ++leadingZeroCount) { 11957 if ((this.word & 0x80000000 >>> leadingZeroCount) !== 0) { 11958 // the first bit of working word is 1 11959 this.word <<= leadingZeroCount; 11960 this.bitsAvailable -= leadingZeroCount; 11961 return leadingZeroCount; 11962 } 11963 } 11964 // we exhausted word and still have not found a 1 11965 this.loadWord(); 11966 return leadingZeroCount + this.skipLZ(); 11967 } 11968 11969 // ():void 11970 skipUEG() { 11971 this.skipBits(1 + this.skipLZ()); 11972 } 11973 11974 // ():void 11975 skipEG() { 11976 this.skipBits(1 + this.skipLZ()); 11977 } 11978 11979 // ():uint 11980 readUEG() { 11981 const clz = this.skipLZ(); // :uint 11982 return this.readBits(clz + 1) - 1; 11983 } 11984 11985 // ():int 11986 readEG() { 11987 const valu = this.readUEG(); // :int 11988 if (0x01 & valu) { 11989 // the number is odd if the low order bit is set 11990 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 11991 } else { 11992 return -1 * (valu >>> 1); // divide by two then make it negative 11993 } 11994 } 11995 11996 // Some convenience functions 11997 // :Boolean 11998 readBoolean() { 11999 return this.readBits(1) === 1; 12000 } 12001 12002 // ():int 12003 readUByte() { 12004 return this.readBits(8); 12005 } 12006 12007 // ():int 12008 readUShort() { 12009 return this.readBits(16); 12010 } 12011 12012 // ():int 12013 readUInt() { 12014 return this.readBits(32); 12015 } 12016 12017 /** 12018 * Advance the ExpGolomb decoder past a scaling list. The scaling 12019 * list is optionally transmitted as part of a sequence parameter 12020 * set and is not relevant to transmuxing. 12021 * @param count the number of entries in this scaling list 12022 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 12023 */ 12024 skipScalingList(count) { 12025 let lastScale = 8; 12026 let nextScale = 8; 12027 let deltaScale; 12028 for (let j = 0; j < count; j++) { 12029 if (nextScale !== 0) { 12030 deltaScale = this.readEG(); 12031 nextScale = (lastScale + deltaScale + 256) % 256; 12032 } 12033 lastScale = nextScale === 0 ? lastScale : nextScale; 12034 } 12035 } 12036 12037 /** 12038 * Read a sequence parameter set and return some interesting video 12039 * properties. A sequence parameter set is the H264 metadata that 12040 * describes the properties of upcoming video frames. 12041 * @returns an object with configuration parsed from the 12042 * sequence parameter set, including the dimensions of the 12043 * associated video frames. 12044 */ 12045 readSPS() { 12046 let frameCropLeftOffset = 0; 12047 let frameCropRightOffset = 0; 12048 let frameCropTopOffset = 0; 12049 let frameCropBottomOffset = 0; 12050 let numRefFramesInPicOrderCntCycle; 12051 let scalingListCount; 12052 let i; 12053 const readUByte = this.readUByte.bind(this); 12054 const readBits = this.readBits.bind(this); 12055 const readUEG = this.readUEG.bind(this); 12056 const readBoolean = this.readBoolean.bind(this); 12057 const skipBits = this.skipBits.bind(this); 12058 const skipEG = this.skipEG.bind(this); 12059 const skipUEG = this.skipUEG.bind(this); 12060 const skipScalingList = this.skipScalingList.bind(this); 12061 readUByte(); 12062 const profileIdc = readUByte(); // profile_idc 12063 readBits(5); // profileCompat constraint_set[0-4]_flag, u(5) 12064 skipBits(3); // reserved_zero_3bits u(3), 12065 readUByte(); // level_idc u(8) 12066 skipUEG(); // seq_parameter_set_id 12067 // some profiles have more optional data we don't need 12068 if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) { 12069 const chromaFormatIdc = readUEG(); 12070 if (chromaFormatIdc === 3) { 12071 skipBits(1); 12072 } // separate_colour_plane_flag 12073 12074 skipUEG(); // bit_depth_luma_minus8 12075 skipUEG(); // bit_depth_chroma_minus8 12076 skipBits(1); // qpprime_y_zero_transform_bypass_flag 12077 if (readBoolean()) { 12078 // seq_scaling_matrix_present_flag 12079 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 12080 for (i = 0; i < scalingListCount; i++) { 12081 if (readBoolean()) { 12082 // seq_scaling_list_present_flag[ i ] 12083 if (i < 6) { 12084 skipScalingList(16); 12085 } else { 12086 skipScalingList(64); 12087 } 12088 } 12089 } 12090 } 12091 } 12092 skipUEG(); // log2_max_frame_num_minus4 12093 const picOrderCntType = readUEG(); 12094 if (picOrderCntType === 0) { 12095 readUEG(); // log2_max_pic_order_cnt_lsb_minus4 12096 } else if (picOrderCntType === 1) { 12097 skipBits(1); // delta_pic_order_always_zero_flag 12098 skipEG(); // offset_for_non_ref_pic 12099 skipEG(); // offset_for_top_to_bottom_field 12100 numRefFramesInPicOrderCntCycle = readUEG(); 12101 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 12102 skipEG(); 12103 } // offset_for_ref_frame[ i ] 12104 } 12105 skipUEG(); // max_num_ref_frames 12106 skipBits(1); // gaps_in_frame_num_value_allowed_flag 12107 const picWidthInMbsMinus1 = readUEG(); 12108 const picHeightInMapUnitsMinus1 = readUEG(); 12109 const frameMbsOnlyFlag = readBits(1); 12110 if (frameMbsOnlyFlag === 0) { 12111 skipBits(1); 12112 } // mb_adaptive_frame_field_flag 12113 12114 skipBits(1); // direct_8x8_inference_flag 12115 if (readBoolean()) { 12116 // frame_cropping_flag 12117 frameCropLeftOffset = readUEG(); 12118 frameCropRightOffset = readUEG(); 12119 frameCropTopOffset = readUEG(); 12120 frameCropBottomOffset = readUEG(); 12121 } 12122 let pixelRatio = [1, 1]; 12123 if (readBoolean()) { 12124 // vui_parameters_present_flag 12125 if (readBoolean()) { 12126 // aspect_ratio_info_present_flag 12127 const aspectRatioIdc = readUByte(); 12128 switch (aspectRatioIdc) { 12129 case 1: 12130 pixelRatio = [1, 1]; 12131 break; 12132 case 2: 12133 pixelRatio = [12, 11]; 12134 break; 12135 case 3: 12136 pixelRatio = [10, 11]; 12137 break; 12138 case 4: 12139 pixelRatio = [16, 11]; 12140 break; 12141 case 5: 12142 pixelRatio = [40, 33]; 12143 break; 12144 case 6: 12145 pixelRatio = [24, 11]; 12146 break; 12147 case 7: 12148 pixelRatio = [20, 11]; 12149 break; 12150 case 8: 12151 pixelRatio = [32, 11]; 12152 break; 12153 case 9: 12154 pixelRatio = [80, 33]; 12155 break; 12156 case 10: 12157 pixelRatio = [18, 11]; 12158 break; 12159 case 11: 12160 pixelRatio = [15, 11]; 12161 break; 12162 case 12: 12163 pixelRatio = [64, 33]; 12164 break; 12165 case 13: 12166 pixelRatio = [160, 99]; 12167 break; 12168 case 14: 12169 pixelRatio = [4, 3]; 12170 break; 12171 case 15: 12172 pixelRatio = [3, 2]; 12173 break; 12174 case 16: 12175 pixelRatio = [2, 1]; 12176 break; 12177 case 255: 12178 { 12179 pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()]; 12180 break; 12181 } 12182 } 12183 } 12184 } 12185 return { 12186 width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2), 12187 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset), 12188 pixelRatio: pixelRatio 12189 }; 12190 } 12191 readSliceType() { 12192 // skip NALu type 12193 this.readUByte(); 12194 // discard first_mb_in_slice 12195 this.readUEG(); 12196 // return slice_type 12197 return this.readUEG(); 12198 } 12199} 12200 12201class AvcVideoParser extends BaseVideoParser { 12202 parseAVCPES(track, textTrack, pes, last, duration) { 12203 const units = this.parseAVCNALu(track, pes.data); 12204 let VideoSample = this.VideoSample; 12205 let push; 12206 let spsfound = false;
12207 // free pes.data to save up some memory 12208 pes.data = null; 12209 12210 // if new NAL units found and last sample still there, let's push ... 12211 // this helps parsing streams with missing AUD (only do this if AUD never found) 12212 if (VideoSample && units.length && !track.audFound) { 12213 this.pushAccessUnit(VideoSample, track); 12214 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts, ''); 12215 } 12216 units.forEach(unit => { 12217 var _VideoSample2; 12218 switch (unit.type) { 12219 // NDR 12220 case 1: 12221 { 12222 let iskey = false; 12223 push = true; 12224 const data = unit.data; 12225 // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...) 12226 if (spsfound && data.length > 4) { 12227 // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR 12228 const sliceType = new ExpGolomb(data).readSliceType(); 12229 // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice 12230 // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples. 12231 // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice. 12232 // I slice: A slice that is not an SI slice that is decoded using intra prediction only. 12233 // if (sliceType === 2 || sliceType === 7) { 12234 if (sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9) { 12235 iskey = true; 12236 } 12237 } 12238 if (iskey) { 12239 var _VideoSample; 12240 // if we have non-keyframe data already, that cannot belong to the same frame as a keyframe, so force a push 12241 if ((_VideoSample = VideoSample) != null && _VideoSample.frame && !VideoSample.key) { 12242 this.pushAccessUnit(VideoSample, track); 12243 VideoSample = this.VideoSample = null; 12244 } 12245 } 12246 if (!VideoSample) { 12247 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts, ''); 12248 } 12249 VideoSample.frame = true; 12250 VideoSample.key = iskey; 12251 break; 12252 // IDR 12253 } 12254 case 5: 12255 push = true; 12256 // handle PES not starting with AUD 12257 // if we have frame data already, that cannot belong to the same frame, so force a push 12258 if ((_VideoSample2 = VideoSample) != null && _VideoSample2.frame && !VideoSample.key) { 12259 this.pushAccessUnit(VideoSample, track); 12260 VideoSample = this.VideoSample = null; 12261 } 12262 if (!VideoSample) { 12263 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts, ''); 12264 } 12265 VideoSample.key = true; 12266 VideoSample.frame = true; 12267 break; 12268 // SEI 12269 case 6: 12270 { 12271 push = true; 12272 parseSEIMessageFromNALu(unit.data, 1, pes.pts, textTrack.samples); 12273 break; 12274 // SPS 12275 } 12276 case 7: 12277 { 12278 var _track$pixelRatio, _track$pixelRatio2; 12279 push = true; 12280 spsfound = true; 12281 const sps = unit.data; 12282 const expGolombDecoder = new ExpGolomb(sps); 12283 const config = expGolombDecoder.readSPS(); 12284 if (!track.sps || track.width !== config.width || track.height !== config.height || ((_track$pixelRatio = track.pixelRatio) == null ? void 0 : _track$pixelRatio[0]) !== config.pixelRatio[0] || ((_track$pixelRatio2 = track.pixelRatio) == null ? void 0 : _track$pixelRatio2[1]) !== config.pixelRatio[1]) { 12285 track.width = config.width; 12286 track.height = config.height; 12287 track.pixelRatio = config.pixelRatio; 12288 track.sps = [sps]; 12289 track.duration = duration; 12290 const codecarray = sps.subarray(1, 4); 12291 let codecstring = 'avc1.'; 12292 for (let i = 0; i < 3; i++) { 12293 let h = codecarray[i].toString(16); 12294 if (h.length < 2) { 12295 h = '0' + h; 12296 } 12297 codecstring += h; 12298 } 12299 track.codec = codecstring; 12300 } 12301 break; 12302 } 12303 // PPS 12304 case 8: 12305 push = true; 12306 track.pps = [unit.data]; 12307 break; 12308 // AUD 12309 case 9: 12310 push = true; 12311 track.audFound = true; 12312 if (VideoSample) { 12313 this.pushAccessUnit(VideoSample, track); 12314 } 12315 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pe
vendor: 8,181 bytes, lines 12315-12552
12315s.dts, ''); 12316 break; 12317 // Filler Data 12318 case 12: 12319 push = true; 12320 break; 12321 default: 12322 push = false; 12323 if (VideoSample) { 12324 VideoSample.debug += 'unknown NAL ' + unit.type + ' '; 12325 } 12326 break; 12327 } 12328 if (VideoSample && push) { 12329 const units = VideoSample.units; 12330 units.push(unit); 12331 } 12332 }); 12333 // if last PES packet, push samples 12334 if (last && VideoSample) { 12335 this.pushAccessUnit(VideoSample, track); 12336 this.VideoSample = null; 12337 } 12338 } 12339 parseAVCNALu(track, array) { 12340 const len = array.byteLength; 12341 let state = track.naluState || 0; 12342 const lastState = state; 12343 const units = []; 12344 let i = 0; 12345 let value; 12346 let overflow; 12347 let unitType; 12348 let lastUnitStart = -1; 12349 let lastUnitType = 0; 12350 // logger.log('PES:' + Hex.hexDump(array)); 12351 12352 if (state === -1) { 12353 // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet 12354 lastUnitStart = 0; 12355 // NALu type is value read from offset 0 12356 lastUnitType = array[0] & 0x1f; 12357 state = 0; 12358 i = 1; 12359 } 12360 while (i < len) { 12361 value = array[i++]; 12362 // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case 12363 if (!state) { 12364 state = value ? 0 : 1; 12365 continue; 12366 } 12367 if (state === 1) { 12368 state = value ? 0 : 2; 12369 continue; 12370 } 12371 // here we have state either equal to 2 or 3 12372 if (!value) { 12373 state = 3; 12374 } else if (value === 1) { 12375 overflow = i - state - 1; 12376 if (lastUnitStart >= 0) { 12377 const unit = { 12378 data: array.subarray(lastUnitStart, overflow), 12379 type: lastUnitType 12380 }; 12381 // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength); 12382 units.push(unit); 12383 } else { 12384 // lastUnitStart is undefined => this is the first start code found in this PES packet 12385 // first check if start code delimiter is overlapping between 2 PES packets, 12386 // ie it started in last packet (lastState not zero) 12387 // and ended at the beginning of this PES packet (i <= 4 - lastState) 12388 const lastUnit = this.getLastNalUnit(track.samples); 12389 if (lastUnit) { 12390 if (lastState && i <= 4 - lastState) { 12391 // start delimiter overlapping between PES packets 12392 // strip start delimiter bytes from the end of last NAL unit 12393 // check if lastUnit had a state different from zero 12394 if (lastUnit.state) { 12395 // strip last bytes 12396 lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState); 12397 } 12398 } 12399 // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit. 12400 12401 if (overflow > 0) { 12402 // logger.log('first NALU found with overflow:' + overflow); 12403 lastUnit.data = appendUint8Array(lastUnit.data, array.subarray(0, overflow)); 12404 lastUnit.state = 0; 12405 } 12406 } 12407 } 12408 // check if we can read unit type 12409 if (i < len) { 12410 unitType = array[i] & 0x1f; 12411 // logger.log('find NALU @ offset:' + i + ',type:' + unitType); 12412 lastUnitStart = i; 12413 lastUnitType = unitType; 12414 state = 0; 12415 } else { 12416 // not enough byte to read unit type. let's read it on next PES parsing 12417 state = -1; 12418 } 12419 } else { 12420 state = 0; 12421 } 12422 } 12423 if (lastUnitStart >= 0 && state >= 0) { 12424 const unit = { 12425 data: array.subarray(lastUnitStart, len), 12426 type: lastUnitType, 12427 state: state 12428 }; 12429 units.push(unit); 12430 // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state); 12431 } 12432 // no NALu found 12433 if (units.length === 0) { 12434 // append pes.data to previous NAL unit 12435 const lastUnit = this.getLastNalUnit(track.samples); 12436 if (lastUnit) { 12437 lastUnit.data = appendUint8Array(lastUnit.data, array); 12438 } 12439 } 12440 track.naluState = state; 12441 return units; 12442 } 12443} 12444 12445/** 12446 * SAMPLE-AES decrypter 12447 */ 12448 12449class SampleAesDecrypter { 12450 constructor(observer, config, keyData) { 12451 this.keyData = void 0; 12452 this.decrypter = void 0; 12453 this.keyData = keyData; 12454 this.decrypter = new Decrypter(config, { 12455 removePKCS7Padding: false 12456 }); 12457 } 12458 decryptBuffer(encryptedData) { 12459 return this.decrypter.decrypt(encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer); 12460 } 12461 12462 // AAC - encrypt all full 16 bytes blocks starting from offset 16 12463 decryptAacSample(samples, sampleIndex, callback) { 12464 const curUnit = samples[sampleIndex].unit; 12465 if (curUnit.length <= 16) { 12466 // No encrypted portion in this sample (first 16 bytes is not 12467 // encrypted, see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/HLS_Sample_Encryption/Encryption/Encryption.html), 12468 return; 12469 } 12470 const encryptedData = curUnit.subarray(16, curUnit.length - curUnit.length % 16); 12471 const encryptedBuffer = encryptedData.buffer.slice(encryptedData.byteOffset, encryptedData.byteOffset + encryptedData.length); 12472 this.decryptBuffer(encryptedBuffer).then(decryptedBuffer => { 12473 const decryptedData = new Uint8Array(decryptedBuffer); 12474 curUnit.set(decryptedData, 16); 12475 if (!this.decrypter.isSync()) { 12476 this.decryptAacSamples(samples, sampleIndex + 1, callback); 12477 } 12478 }); 12479 } 12480 decryptAacSamples(samples, sampleIndex, callback) { 12481 for (;; sampleIndex++) { 12482 if (sampleIndex >= samples.length) { 12483 callback(); 12484 return; 12485 } 12486 if (samples[sampleIndex].unit.length < 32) { 12487 continue; 12488 } 12489 this.decryptAacSample(samples, sampleIndex, callback); 12490 if (!this.decrypter.isSync()) { 12491 return; 12492 } 12493 } 12494 } 12495 12496 // AVC - encrypt one 16 bytes block out of ten, starting from offset 32 12497 getAvcEncryptedData(decodedData) { 12498 const encryptedDataLen = Math.floor((decodedData.length - 48) / 160) * 16 + 16; 12499 const encryptedData = new Int8Array(encryptedDataLen); 12500 let outputPos = 0; 12501 for (let inputPos = 32; inputPos < decodedData.length - 16; inputPos += 160, outputPos += 16) { 12502 encryptedData.set(decodedData.subarray(inputPos, inputPos + 16), outputPos); 12503 } 12504 return encryptedData; 12505 } 12506 getAvcDecryptedUnit(decodedData, decryptedData) { 12507 const uint8DecryptedData = new Uint8Array(decryptedData); 12508 let inputPos = 0; 12509 for (let outputPos = 32; outputPos < decodedData.length - 16; outputPos += 160, inputPos += 16) { 12510 decodedData.set(uint8DecryptedData.subarray(inputPos, inputPos + 16), outputPos); 12511 } 12512 return decodedData; 12513 } 12514 decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit) { 12515 const decodedData = discardEPB(curUnit.data); 12516 const encryptedData = this.getAvcEncryptedData(decodedData); 12517 this.decryptBuffer(encryptedData.buffer).then(decryptedBuffer => { 12518 curUnit.data = this.getAvcDecryptedUnit(decodedData, decryptedBuffer); 12519 if (!this.decrypter.isSync()) { 12520 this.decryptAvcSamples(samples, sampleIndex, unitIndex + 1, callback); 12521 } 12522 }); 12523 } 12524 decryptAvcSamples(samples, sampleIndex, unitIndex, callback) { 12525 if (samples instanceof Uint8Array) { 12526 throw new Error('Cannot decrypt samples of type Uint8Array'); 12527 } 12528 for (;; sampleIndex++, unitIndex = 0) { 12529 if (sampleIndex >= samples.length) { 12530 callback(); 12531 return; 12532 } 12533 const curUnits = samples[sampleIndex].units; 12534 for (;; unitIndex++) { 12535 if (unitIndex >= curUnits.length) { 12536 break; 12537 } 12538 const curUnit = curUnits[unitIndex]; 12539 if (curUnit.data.length <= 48 || curUnit.type !== 1 && curUnit.type !== 5) { 12540 continue; 12541 } 12542 this.decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit); 12543 if (!this.decrypter.isSync()) { 12544 return; 12545 } 12546 } 12547 } 12548 } 12549} 12550 12551const PACKET_LENGTH = 188; 12552class TSDemuxer {
vendor: 5,984 bytes, lines 12553-12737
12553 constructor(observer, config, typeSupported) { 12554 this.observer = void 0; 12555 this.config = void 0; 12556 this.typeSupported = void 0; 12557 this.sampleAes = null; 12558 this.pmtParsed = false; 12559 this.audioCodec = void 0; 12560 this.videoCodec = void 0; 12561 this._duration = 0; 12562 this._pmtId = -1; 12563 this._videoTrack = void 0; 12564 this._audioTrack = void 0; 12565 this._id3Track = void 0; 12566 this._txtTrack = void 0; 12567 this.aacOverFlow = null; 12568 this.remainderData = null; 12569 this.videoParser = void 0; 12570 this.observer = observer; 12571 this.config = config; 12572 this.typeSupported = typeSupported; 12573 this.videoParser = new AvcVideoParser(); 12574 } 12575 static probe(data) { 12576 const syncOffset = TSDemuxer.syncOffset(data); 12577 if (syncOffset > 0) { 12578 logger.warn(`MPEG2-TS detected but first sync word found @ offset ${syncOffset}`); 12579 } 12580 return syncOffset !== -1; 12581 } 12582 static syncOffset(data) { 12583 const length = data.length; 12584 let scanwindow = Math.min(PACKET_LENGTH * 5, length - PACKET_LENGTH) + 1; 12585 let i = 0; 12586 while (i < scanwindow) { 12587 // a TS init segment should contain at least 2 TS packets: PAT and PMT, each starting with 0x47 12588 let foundPat = false; 12589 let packetStart = -1; 12590 let tsPackets = 0; 12591 for (let j = i; j < length; j += PACKET_LENGTH) { 12592 if (data[j] === 0x47 && (length - j === PACKET_LENGTH || data[j + PACKET_LENGTH] === 0x47)) { 12593 tsPackets++; 12594 if (packetStart === -1) { 12595 packetStart = j; 12596 // First sync word found at offset, increase scan length (#5251) 12597 if (packetStart !== 0) { 12598 scanwindow = Math.min(packetStart + PACKET_LENGTH * 99, data.length - PACKET_LENGTH) + 1; 12599 } 12600 } 12601 if (!foundPat) { 12602 foundPat = parsePID(data, j) === 0; 12603 } 12604 // Sync word found at 0 with 3 packets, or found at offset least 2 packets up to scanwindow (#5501) 12605 if (foundPat && tsPackets > 1 && (packetStart === 0 && tsPackets > 2 || j + PACKET_LENGTH > scanwindow)) { 12606 return packetStart; 12607 } 12608 } else if (tsPackets) { 12609 // Exit if sync word found, but does not contain contiguous packets 12610 return -1; 12611 } else { 12612 break; 12613 } 12614 } 12615 i++; 12616 } 12617 return -1; 12618 } 12619 12620 /** 12621 * Creates a track model internal to demuxer used to drive remuxing input 12622 */ 12623 static createTrack(type, duration) { 12624 return { 12625 container: type === 'video' || type === 'audio' ? 'video/mp2t' : undefined, 12626 type, 12627 id: RemuxerTrackIdConfig[type], 12628 pid: -1, 12629 inputTimeScale: 90000, 12630 sequenceNumber: 0, 12631 samples: [], 12632 dropped: 0, 12633 duration: type === 'audio' ? duration : undefined 12634 }; 12635 } 12636 12637 /** 12638 * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start) 12639 * Resets all internal track instances of the demuxer. 12640 */ 12641 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 12642 this.pmtParsed = false; 12643 this._pmtId = -1; 12644 this._videoTrack = TSDemuxer.createTrack('video'); 12645 this._audioTrack = TSDemuxer.createTrack('audio', trackDuration); 12646 this._id3Track = TSDemuxer.createTrack('id3'); 12647 this._txtTrack = TSDemuxer.createTrack('text'); 12648 this._audioTrack.segmentCodec = 'aac'; 12649 12650 // flush any partial content 12651 this.aacOverFlow = null; 12652 this.remainderData = null; 12653 this.audioCodec = audioCodec; 12654 this.videoCodec = videoCodec; 12655 this._duration = trackDuration; 12656 } 12657 resetTimeStamp() {} 12658 resetContiguity() { 12659 const { 12660 _audioTrack, 12661 _videoTrack, 12662 _id3Track 12663 } = this; 12664 if (_audioTrack) { 12665 _audioTrack.pesData = null; 12666 } 12667 if (_videoTrack) { 12668 _videoTrack.pesData = null; 12669 } 12670 if (_id3Track) { 12671 _id3Track.pesData = null; 12672 } 12673 this.aacOverFlow = null; 12674 this.remainderData = null; 12675 } 12676 demux(data, timeOffset, isSampleAes = false, flush = false) { 12677 if (!isSampleAes) { 12678 this.sampleAes = null; 12679 } 12680 let pes; 12681 const videoTrack = this._videoTrack; 12682 const audioTrack = this._audioTrack; 12683 const id3Track = this._id3Track; 12684 const textTrack = this._txtTrack; 12685 let videoPid = videoTrack.pid; 12686 let videoData = videoTrack.pesData; 12687 let audioPid = audioTrack.pid; 12688 let id3Pid = id3Track.pid; 12689 let audioData = audioTrack.pesData; 12690 let id3Data = id3Track.pesData; 12691 let unknownPID = null; 12692 let pmtParsed = this.pmtParsed; 12693 let pmtId = this._pmtId; 12694 let len = data.length; 12695 if (this.remainderData) { 12696 data = appendUint8Array(this.remainderData, data); 12697 len = data.length; 12698 this.remainderData = null; 12699 } 12700 if (len < PACKET_LENGTH && !flush) { 12701 this.remainderData = data; 12702 return { 12703 audioTrack, 12704 videoTrack, 12705 id3Track, 12706 textTrack 12707 }; 12708 } 12709 const syncOffset = Math.max(0, TSDemuxer.syncOffset(data)); 12710 len -= (len - syncOffset) % PACKET_LENGTH; 12711 if (len < data.byteLength && !flush) { 12712 this.remainderData = new Uint8Array(data.buffer, len, data.buffer.byteLength - len); 12713 } 12714 12715 // loop through TS packets 12716 let tsPacketErrors = 0; 12717 for (let start = syncOffset; start < len; start += PACKET_LENGTH) { 12718 if (data[start] === 0x47) { 12719 const stt = !!(data[start + 1] & 0x40); 12720 const pid = parsePID(data, start); 12721 const atf = (data[start + 3] & 0x30) >> 4; 12722 12723 // if an adaption field is present, its length is specified by the fifth byte of the TS packet header. 12724 let offset; 12725 if (atf > 1) { 12726 offset = start + 5 + data[start + 4]; 12727 // continue if there is only adaptation field 12728 if (offset === start + PACKET_LENGTH) { 12729 continue; 12730 } 12731 } else { 12732 offset = start + 4; 12733 } 12734 switch (pid) { 12735 case videoPid: 12736 if (stt) { 12737 if (videoData && (pes = parsePES(videoData))) {
12738 this.videoParser.parseAVCPES(videoTrack, textTrack, pes, false, this._duration); 12739 } 12740 videoData = { 12741 data: [], 12742 size: 0 12743 }; 12744 } 12745 if (videoData) { 12746 videoData.data.push(data.subarray(offset, start + PACKET_LENGTH)); 12747 videoData.size += start + PACKET_LENGTH - offset; 12748 } 12749 break; 12750 case audioPid: 12751 if (stt) { 12752 if (audioData && (pes = parsePES(audioData))) { 12753 switch (audioTrack.segmentCodec) { 12754 case 'aac': 12755 this.parseAACPES(audioTrack, pes); 12756 break; 12757 case 'mp3': 12758 this.parseMPEGPES(audioTrack, pes); 12759 break; 12760 case 'ac3': 12761 { 12762 this.parseAC3PES(audioTrack, pes); 12763 } 12764 break; 12765 } 12766 } 12767 audioData = { 12768 data: [], 12769 size: 0 12770 }; 12771 } 12772 if (audioData) { 12773 audioData.data.push(data.subarray(offset, start + PACKET_LENGTH)); 12774 audioData.size += start + PACKET_LENGTH - offset; 12775 } 12776 break; 12777 case id3Pid: 12778 if (stt) { 12779 if (id3Data && (pes = parsePES(id3Data))) { 12780 this.parseID3PES(id3Track, pes); 12781 } 12782 id3Data = { 12783 data: [], 12784 size: 0 12785 }; 12786 } 12787 if (id3Data) { 12788 id3Data.data.push(data.subarray(offset, start + PACKET_LENGTH)); 12789 id3Data.size += start + PACKET_LENGTH - offset; 12790 } 12791 break; 12792 case 0: 12793 if (stt) { 12794 offset += data[offset] + 1; 12795 } 12796 pmtId = this._pmtId = parsePAT(data, offset); 12797 // logger.log('PMT PID:' + this._pmtId); 12798 break; 12799 case pmtId: 12800 { 12801 if (stt) { 12802 offset += data[offset] + 1; 12803 } 12804 const parsedPIDs = parsePMT(data, offset, this.typeSupported, isSampleAes, this.observer); 12805 12806 // only update track id if track PID found while parsing PMT 12807 // this is to avoid resetting the PID to -1 in case 12808 // track PID transiently disappears from the stream 12809 // this could happen in case of transient missing audio samples for example 12810 // NOTE this is only the PID of the track as found in TS, 12811 // but we are not using this for MP4 track IDs. 12812 videoPid = parsedPIDs.videoPid; 12813 if (videoPid > 0) { 12814 videoTrack.pid = videoPid; 12815 videoTrack.segmentCodec = parsedPIDs.segmentVideoCodec; 12816 } 12817 audioPid = parsedPIDs.audioPid; 12818 if (audioPid > 0) { 12819 audioTrack.pid = audioPid; 12820 audioTrack.segmentCodec = parsedPIDs.segmentAudioCodec; 12821 } 12822 id3Pid = parsedPIDs.id3Pid; 12823 if (id3Pid > 0) { 12824 id3Track.pid = id3Pid; 12825 } 12826 if (unknownPID !== null && !pmtParsed) { 12827 logger.warn(`MPEG-TS PMT found at ${start} after unknown PID '${unknownPID}'. Backtracking to sync byte @${syncOffset} to parse all TS packets.`); 12828 unknownPID = null; 12829 // we set it to -188, the += 188 in the for loop will reset start to 0 12830 start = syncOffset - 188; 12831 } 12832 pmtParsed = this.pmtParsed = true; 12833 break; 12834 } 12835 case 0x11: 12836 case 0x1fff: 12837 break; 12838 default: 12839 unknownPID = pid; 12840 break; 12841 } 12842 } else { 12843 tsPacketErrors++; 12844 } 12845 } 12846 if (tsPacketErrors > 0) { 12847 emitParsingError(this.observer, new Error(`Found ${tsPacketErrors} TS packet/s that do not start with 0x47`)); 12848 } 12849 videoTrack.pesData = videoData; 12850 audioTrack.pesData = audioData; 12851 id3Track.pesData = id3Data; 12852 const demuxResult = { 12853 audioTrack, 12854 videoTrack, 12855 id3Track, 12856 textTrack 12857 }; 12858 if (flush) { 12859 this.extractRemainingSamples(demuxResult); 12860 } 12861 return demuxResult; 12862 } 12863 flush() { 12864 const { 12865 remainderData 12866 } = this; 12867 this.remainderData = null; 12868 let result; 12869 if (remainderData) { 12870 result = this.demux(remainderData, -1, false, true); 12871 } else { 12872 result = { 12873 videoTrack: this._videoTrack, 12874 audioTrack: this._audioTrack, 12875 id3Track: this._id3Track, 12876 textTrack: this._txtTrack 12877 }; 12878 } 12879 this.extractRemainingSamples(result); 12880 if (this.sampleAes) { 12881 return this.decrypt(result, this.sampleAes); 12882 } 12883 return result; 12884 } 12885 extractRemainingSamples(demuxResult) { 12886 const { 12887 audioTrack, 12888 videoTrack, 12889 id3Track, 12890 textTrack 12891 } = demuxResult; 12892 const videoData = videoTrack.pesData; 12893 const audioData = audioTrack.pesData; 12894 const id3Data = id3Track.pesData; 12895 // try to parse last PES packets 12896 let pes; 12897 if (videoData && (pes = parsePES(videoData))) {
vendor: 6,356 bytes, lines 12898-13098
12898 this.videoParser.parseAVCPES(videoTrack, textTrack, pes, true, this._duration); 12899 videoTrack.pesData = null; 12900 } else { 12901 // either avcData null or PES truncated, keep it for next frag parsing 12902 videoTrack.pesData = videoData; 12903 } 12904 if (audioData && (pes = parsePES(audioData))) { 12905 switch (audioTrack.segmentCodec) { 12906 case 'aac': 12907 this.parseAACPES(audioTrack, pes); 12908 break; 12909 case 'mp3': 12910 this.parseMPEGPES(audioTrack, pes); 12911 break; 12912 case 'ac3': 12913 { 12914 this.parseAC3PES(audioTrack, pes); 12915 } 12916 break; 12917 } 12918 audioTrack.pesData = null; 12919 } else { 12920 if (audioData != null && audioData.size) { 12921 logger.log('last AAC PES packet truncated,might overlap between fragments'); 12922 } 12923 12924 // either audioData null or PES truncated, keep it for next frag parsing 12925 audioTrack.pesData = audioData; 12926 } 12927 if (id3Data && (pes = parsePES(id3Data))) { 12928 this.parseID3PES(id3Track, pes); 12929 id3Track.pesData = null; 12930 } else { 12931 // either id3Data null or PES truncated, keep it for next frag parsing 12932 id3Track.pesData = id3Data; 12933 } 12934 } 12935 demuxSampleAes(data, keyData, timeOffset) { 12936 const demuxResult = this.demux(data, timeOffset, true, !this.config.progressive); 12937 const sampleAes = this.sampleAes = new SampleAesDecrypter(this.observer, this.config, keyData); 12938 return this.decrypt(demuxResult, sampleAes); 12939 } 12940 decrypt(demuxResult, sampleAes) { 12941 return new Promise(resolve => { 12942 const { 12943 audioTrack, 12944 videoTrack 12945 } = demuxResult; 12946 if (audioTrack.samples && audioTrack.segmentCodec === 'aac') { 12947 sampleAes.decryptAacSamples(audioTrack.samples, 0, () => { 12948 if (videoTrack.samples) { 12949 sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { 12950 resolve(demuxResult); 12951 }); 12952 } else { 12953 resolve(demuxResult); 12954 } 12955 }); 12956 } else if (videoTrack.samples) { 12957 sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { 12958 resolve(demuxResult); 12959 }); 12960 } 12961 }); 12962 } 12963 destroy() { 12964 this._duration = 0; 12965 } 12966 parseAACPES(track, pes) { 12967 let startOffset = 0; 12968 const aacOverFlow = this.aacOverFlow; 12969 let data = pes.data; 12970 if (aacOverFlow) { 12971 this.aacOverFlow = null; 12972 const frameMissingBytes = aacOverFlow.missing; 12973 const sampleLength = aacOverFlow.sample.unit.byteLength; 12974 // logger.log(`AAC: append overflowing ${sampleLength} bytes to beginning of new PES`); 12975 if (frameMissingBytes === -1) { 12976 data = appendUint8Array(aacOverFlow.sample.unit, data); 12977 } else { 12978 const frameOverflowBytes = sampleLength - frameMissingBytes; 12979 aacOverFlow.sample.unit.set(data.subarray(0, frameMissingBytes), frameOverflowBytes); 12980 track.samples.push(aacOverFlow.sample); 12981 startOffset = aacOverFlow.missing; 12982 } 12983 } 12984 // look for ADTS header (0xFFFx) 12985 let offset; 12986 let len; 12987 for (offset = startOffset, len = data.length; offset < len - 1; offset++) { 12988 if (isHeader$1(data, offset)) { 12989 break; 12990 } 12991 } 12992 // if ADTS header does not start straight from the beginning of the PES payload, raise an error 12993 if (offset !== startOffset) { 12994 let reason; 12995 const recoverable = offset < len - 1; 12996 if (recoverable) { 12997 reason = `AAC PES did not start with ADTS header,offset:${offset}`; 12998 } else { 12999 reason = 'No ADTS header found in AAC PES'; 13000 } 13001 emitParsingError(this.observer, new Error(reason), recoverable); 13002 if (!recoverable) { 13003 return; 13004 } 13005 } 13006 initTrackConfig(track, this.observer, data, offset, this.audioCodec); 13007 let pts; 13008 if (pes.pts !== undefined) { 13009 pts = pes.pts; 13010 } else if (aacOverFlow) { 13011 // if last AAC frame is overflowing, we should ensure timestamps are contiguous: 13012 // first sample PTS should be equal to last sample PTS + frameDuration 13013 const frameDuration = getFrameDuration(track.samplerate); 13014 pts = aacOverFlow.sample.pts + frameDuration; 13015 } else { 13016 logger.warn('[tsdemuxer]: AAC PES unknown PTS'); 13017 return; 13018 } 13019 13020 // scan for aac samples 13021 let frameIndex = 0; 13022 let frame; 13023 while (offset < len) { 13024 frame = appendFrame$2(track, data, offset, pts, frameIndex); 13025 offset += frame.length; 13026 if (!frame.missing) { 13027 frameIndex++; 13028 for (; offset < len - 1; offset++) { 13029 if (isHeader$1(data, offset)) { 13030 break; 13031 } 13032 } 13033 } else { 13034 this.aacOverFlow = frame; 13035 break; 13036 } 13037 } 13038 } 13039 parseMPEGPES(track, pes) { 13040 const data = pes.data; 13041 const length = data.length; 13042 let frameIndex = 0; 13043 let offset = 0; 13044 const pts = pes.pts; 13045 if (pts === undefined) { 13046 logger.warn('[tsdemuxer]: MPEG PES unknown PTS'); 13047 return; 13048 } 13049 while (offset < length) { 13050 if (isHeader(data, offset)) { 13051 const frame = appendFrame$1(track, data, offset, pts, frameIndex); 13052 if (frame) { 13053 offset += frame.length; 13054 frameIndex++; 13055 } else { 13056 // logger.log('Unable to parse Mpeg audio frame'); 13057 break; 13058 } 13059 } else { 13060 // nothing found, keep looking 13061 offset++; 13062 } 13063 } 13064 } 13065 parseAC3PES(track, pes) { 13066 { 13067 const data = pes.data; 13068 const pts = pes.pts; 13069 if (pts === undefined) { 13070 logger.warn('[tsdemuxer]: AC3 PES unknown PTS'); 13071 return; 13072 } 13073 const length = data.length; 13074 let frameIndex = 0; 13075 let offset = 0; 13076 let parsed; 13077 while (offset < length && (parsed = appendFrame(track, data, offset, pts, frameIndex++)) > 0) { 13078 offset += parsed; 13079 } 13080 } 13081 } 13082 parseID3PES(id3Track, pes) { 13083 if (pes.pts === undefined) { 13084 logger.warn('[tsdemuxer]: ID3 PES unknown PTS'); 13085 return; 13086 } 13087 const id3Sample = _extends({}, pes, { 13088 type: this._videoTrack ? MetadataSchema.emsg : MetadataSchema.audioId3, 13089 duration: Number.POSITIVE_INFINITY 13090 }); 13091 id3Track.samples.push(id3Sample); 13092 } 13093} 13094function parsePID(data, offset) { 13095 // pid is a 13-bit field starting at the last bit of TS[1] 13096 return ((data[offset + 1] & 0x1f) << 8) + data[offset + 2]; 13097} 13098function parsePAT(data, offset) {
vendor: 5,884 bytes, lines 13099-13272
13099 // skip the PSI header and parse the first PMT entry 13100 return (data[offset + 10] & 0x1f) << 8 | data[offset + 11]; 13101} 13102function parsePMT(data, offset, typeSupported, isSampleAes, observer) { 13103 const result = { 13104 audioPid: -1, 13105 videoPid: -1, 13106 id3Pid: -1, 13107 segmentVideoCodec: 'avc', 13108 segmentAudioCodec: 'aac' 13109 }; 13110 const sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2]; 13111 const tableEnd = offset + 3 + sectionLength - 4; 13112 // to determine where the table is, we have to figure out how 13113 // long the program info descriptors are 13114 const programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11]; 13115 // advance the offset to the first entry in the mapping table 13116 offset += 12 + programInfoLength; 13117 while (offset < tableEnd) { 13118 const pid = parsePID(data, offset); 13119 const esInfoLength = (data[offset + 3] & 0x0f) << 8 | data[offset + 4]; 13120 switch (data[offset]) { 13121 case 0xcf: 13122 // SAMPLE-AES AAC 13123 if (!isSampleAes) { 13124 logEncryptedSamplesFoundInUnencryptedStream('ADTS AAC'); 13125 break; 13126 } 13127 /* falls through */ 13128 case 0x0f: 13129 // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio) 13130 // logger.log('AAC PID:' + pid); 13131 if (result.audioPid === -1) { 13132 result.audioPid = pid; 13133 } 13134 break; 13135 13136 // Packetized metadata (ID3) 13137 case 0x15: 13138 // logger.log('ID3 PID:' + pid); 13139 if (result.id3Pid === -1) { 13140 result.id3Pid = pid; 13141 } 13142 break; 13143 case 0xdb: 13144 // SAMPLE-AES AVC 13145 if (!isSampleAes) { 13146 logEncryptedSamplesFoundInUnencryptedStream('H.264'); 13147 break; 13148 } 13149 /* falls through */ 13150 case 0x1b: 13151 // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video) 13152 // logger.log('AVC PID:' + pid); 13153 if (result.videoPid === -1) { 13154 result.videoPid = pid; 13155 result.segmentVideoCodec = 'avc'; 13156 } 13157 break; 13158 13159 // ISO/IEC 11172-3 (MPEG-1 audio) 13160 // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio) 13161 case 0x03: 13162 case 0x04: 13163 // logger.log('MPEG PID:' + pid); 13164 if (!typeSupported.mpeg && !typeSupported.mp3) { 13165 logger.log('MPEG audio found, not supported in this browser'); 13166 } else if (result.audioPid === -1) { 13167 result.audioPid = pid; 13168 result.segmentAudioCodec = 'mp3'; 13169 } 13170 break; 13171 case 0xc1: 13172 // SAMPLE-AES AC3 13173 if (!isSampleAes) { 13174 logEncryptedSamplesFoundInUnencryptedStream('AC-3'); 13175 break; 13176 } 13177 /* falls through */ 13178 case 0x81: 13179 { 13180 if (!typeSupported.ac3) { 13181 logger.log('AC-3 audio found, not supported in this browser'); 13182 } else if (result.audioPid === -1) { 13183 result.audioPid = pid; 13184 result.segmentAudioCodec = 'ac3'; 13185 } 13186 } 13187 break; 13188 case 0x06: 13189 // stream_type 6 can mean a lot of different things in case of DVB. 13190 // We need to look at the descriptors. Right now, we're only interested 13191 // in AC-3 audio, so we do the descriptor parsing only when we don't have 13192 // an audio PID yet. 13193 if (result.audioPid === -1 && esInfoLength > 0) { 13194 let parsePos = offset + 5; 13195 let remaining = esInfoLength; 13196 while (remaining > 2) { 13197 const descriptorId = data[parsePos]; 13198 switch (descriptorId) { 13199 case 0x6a: 13200 // DVB Descriptor for AC-3 13201 { 13202 if (typeSupported.ac3 !== true) { 13203 logger.log('AC-3 audio found, not supported in this browser for now'); 13204 } else { 13205 result.audioPid = pid; 13206 result.segmentAudioCodec = 'ac3'; 13207 } 13208 } 13209 break; 13210 } 13211 const descriptorLen = data[parsePos + 1] + 2; 13212 parsePos += descriptorLen; 13213 remaining -= descriptorLen; 13214 } 13215 } 13216 break; 13217 case 0xc2: // SAMPLE-AES EC3 13218 /* falls through */ 13219 case 0x87: 13220 emitParsingError(observer, new Error('Unsupported EC-3 in M2TS found')); 13221 return result; 13222 case 0x24: 13223 emitParsingError(observer, new Error('Unsupported HEVC in M2TS found')); 13224 return result; 13225 } 13226 // move to the next table entry 13227 // skip past the elementary stream descriptors, if present 13228 offset += esInfoLength + 5; 13229 } 13230 return result; 13231} 13232function emitParsingError(observer, error, levelRetry) { 13233 logger.warn(`parsing error: ${error.message}`); 13234 observer.emit(Events.ERROR, Events.ERROR, { 13235 type: ErrorTypes.MEDIA_ERROR, 13236 details: ErrorDetails.FRAG_PARSING_ERROR, 13237 fatal: false, 13238 levelRetry, 13239 error, 13240 reason: error.message 13241 }); 13242} 13243function logEncryptedSamplesFoundInUnencryptedStream(type) { 13244 logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`); 13245} 13246function parsePES(stream) { 13247 let i = 0; 13248 let frag; 13249 let pesLen; 13250 let pesHdrLen; 13251 let pesPts; 13252 let pesDts; 13253 const data = stream.data; 13254 // safety check 13255 if (!stream || stream.size === 0) { 13256 return null; 13257 } 13258 13259 // we might need up to 19 bytes to read PES header 13260 // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes 13261 // usually only one merge is needed (and this is rare ...) 13262 while (data[0].length < 19 && data.length > 1) { 13263 data[0] = appendUint8Array(data[0], data[1]); 13264 data.splice(1, 1); 13265 } 13266 // retrieve PTS/DTS from first fragment 13267 frag = data[0]; 13268 const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2]; 13269 if (pesPrefix === 1) { 13270 pesLen = (frag[4] << 8) + frag[5]; 13271 // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated 13272 // minus 6 : PES header size
vendor: 16,135 bytes, lines 13273-13743
13273 if (pesLen && pesLen > stream.size - 6) { 13274 return null; 13275 } 13276 const pesFlags = frag[7]; 13277 if (pesFlags & 0xc0) { 13278 /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 13279 as PTS / DTS is 33 bit we cannot use bitwise operator in JS, 13280 as Bitwise operators treat their operands as a sequence of 32 bits */ 13281 pesPts = (frag[9] & 0x0e) * 536870912 + 13282 // 1 << 29 13283 (frag[10] & 0xff) * 4194304 + 13284 // 1 << 22 13285 (frag[11] & 0xfe) * 16384 + 13286 // 1 << 14 13287 (frag[12] & 0xff) * 128 + 13288 // 1 << 7 13289 (frag[13] & 0xfe) / 2; 13290 if (pesFlags & 0x40) { 13291 pesDts = (frag[14] & 0x0e) * 536870912 + 13292 // 1 << 29 13293 (frag[15] & 0xff) * 4194304 + 13294 // 1 << 22 13295 (frag[16] & 0xfe) * 16384 + 13296 // 1 << 14 13297 (frag[17] & 0xff) * 128 + 13298 // 1 << 7 13299 (frag[18] & 0xfe) / 2; 13300 if (pesPts - pesDts > 60 * 90000) { 13301 logger.warn(`${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`); 13302 pesPts = pesDts; 13303 } 13304 } else { 13305 pesDts = pesPts; 13306 } 13307 } 13308 pesHdrLen = frag[8]; 13309 // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension 13310 let payloadStartOffset = pesHdrLen + 9; 13311 if (stream.size <= payloadStartOffset) { 13312 return null; 13313 } 13314 stream.size -= payloadStartOffset; 13315 // reassemble PES packet 13316 const pesData = new Uint8Array(stream.size); 13317 for (let j = 0, dataLen = data.length; j < dataLen; j++) { 13318 frag = data[j]; 13319 let len = frag.byteLength; 13320 if (payloadStartOffset) { 13321 if (payloadStartOffset > len) { 13322 // trim full frag if PES header bigger than frag 13323 payloadStartOffset -= len; 13324 continue; 13325 } else { 13326 // trim partial frag if PES header smaller than frag 13327 frag = frag.subarray(payloadStartOffset); 13328 len -= payloadStartOffset; 13329 payloadStartOffset = 0; 13330 } 13331 } 13332 pesData.set(frag, i); 13333 i += len; 13334 } 13335 if (pesLen) { 13336 // payload size : remove PES header + PES extension 13337 pesLen -= pesHdrLen + 3; 13338 } 13339 return { 13340 data: pesData, 13341 pts: pesPts, 13342 dts: pesDts, 13343 len: pesLen 13344 }; 13345 } 13346 return null; 13347} 13348 13349/** 13350 * MP3 demuxer 13351 */ 13352class MP3Demuxer extends BaseAudioDemuxer { 13353 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 13354 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 13355 this._audioTrack = { 13356 container: 'audio/mpeg', 13357 type: 'audio', 13358 id: 2, 13359 pid: -1, 13360 sequenceNumber: 0, 13361 segmentCodec: 'mp3', 13362 samples: [], 13363 manifestCodec: audioCodec, 13364 duration: trackDuration, 13365 inputTimeScale: 90000, 13366 dropped: 0 13367 }; 13368 } 13369 static probe(data) { 13370 if (!data) { 13371 return false; 13372 } 13373 13374 // check if data contains ID3 timestamp and MPEG sync word 13375 // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 13376 // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) 13377 // More info http://www.mp3-tech.org/programmer/frame_header.html 13378 const id3Data = getID3Data(data, 0); 13379 let offset = (id3Data == null ? void 0 : id3Data.length) || 0; 13380 13381 // Check for ac-3|ec-3 sync bytes and return false if present 13382 if (id3Data && data[offset] === 0x0b && data[offset + 1] === 0x77 && getTimeStamp(id3Data) !== undefined && 13383 // check the bsid to confirm ac-3 or ec-3 (not mp3) 13384 getAudioBSID(data, offset) <= 16) { 13385 return false; 13386 } 13387 for (let length = data.length; offset < length; offset++) { 13388 if (probe(data, offset)) { 13389 logger.log('MPEG Audio sync word found !'); 13390 return true; 13391 } 13392 } 13393 return false; 13394 } 13395 canParse(data, offset) { 13396 return canParse(data, offset); 13397 } 13398 appendFrame(track, data, offset) { 13399 if (this.basePTS === null) { 13400 return; 13401 } 13402 return appendFrame$1(track, data, offset, this.basePTS, this.frameIndex); 13403 } 13404} 13405 13406/** 13407 * AAC helper 13408 */ 13409 13410class AAC { 13411 static getSilentFrame(codec, channelCount) { 13412 switch (codec) { 13413 case 'mp4a.40.2': 13414 if (channelCount === 1) { 13415 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); 13416 } else if (channelCount === 2) { 13417 return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); 13418 } else if (channelCount === 3) { 13419 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); 13420 } else if (channelCount === 4) { 13421 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); 13422 } else if (channelCount === 5) { 13423 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); 13424 } else if (channelCount === 6) { 13425 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); 13426 } 13427 break; 13428 // handle HE-AAC below (mp4a.40.5 / mp4a.40.29) 13429 default: 13430 if (channelCount === 1) { 13431 // ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 13432 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 13433 } else if (channelCount === 2) { 13434 // ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 13435 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 13436 } else if (channelCount === 3) { 13437 // ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac 13438 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 13439 } 13440 break; 13441 } 13442 return undefined; 13443 } 13444} 13445 13446/** 13447 * Generate MP4 Box 13448 */ 13449 13450const UINT32_MAX = Math.pow(2, 32) - 1; 13451class MP4 { 13452 static init() { 13453 MP4.types = { 13454 avc1: [], 13455 // codingname 13456 avcC: [], 13457 btrt: [], 13458 dinf: [], 13459 dref: [], 13460 esds: [], 13461 ftyp: [], 13462 hdlr: [], 13463 mdat: [], 13464 mdhd: [], 13465 mdia: [], 13466 mfhd: [], 13467 minf: [], 13468 moof: [], 13469 moov: [], 13470 mp4a: [], 13471 '.mp3': [], 13472 dac3: [], 13473 'ac-3': [], 13474 mvex: [], 13475 mvhd: [], 13476 pasp: [], 13477 sdtp: [], 13478 stbl: [], 13479 stco: [], 13480 stsc: [], 13481 stsd: [], 13482 stsz: [], 13483 stts: [], 13484 tfdt: [], 13485 tfhd: [], 13486 traf: [], 13487 trak: [], 13488 trun: [], 13489 trex: [], 13490 tkhd: [], 13491 vmhd: [], 13492 smhd: [] 13493 }; 13494 let i; 13495 for (i in MP4.types) { 13496 if (MP4.types.hasOwnProperty(i)) { 13497 MP4.types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 13498 } 13499 } 13500 const videoHdlr = new Uint8Array([0x00, 13501 // version 0 13502 0x00, 0x00, 0x00, 13503 // flags 13504 0x00, 0x00, 0x00, 0x00, 13505 // pre_defined 13506 0x76, 0x69, 0x64, 0x65, 13507 // handler_type: 'vide' 13508 0x00, 0x00, 0x00, 0x00, 13509 // reserved 13510 0x00, 0x00, 0x00, 0x00, 13511 // reserved 13512 0x00, 0x00, 0x00, 0x00, 13513 // reserved 13514 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 13515 ]); 13516 const audioHdlr = new Uint8Array([0x00, 13517 // version 0 13518 0x00, 0x00, 0x00, 13519 // flags 13520 0x00, 0x00, 0x00, 0x00, 13521 // pre_defined 13522 0x73, 0x6f, 0x75, 0x6e, 13523 // handler_type: 'soun' 13524 0x00, 0x00, 0x00, 0x00, 13525 // reserved 13526 0x00, 0x00, 0x00, 0x00, 13527 // reserved 13528 0x00, 0x00, 0x00, 0x00, 13529 // reserved 13530 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 13531 ]); 13532 MP4.HDLR_TYPES = { 13533 video: videoHdlr, 13534 audio: audioHdlr 13535 }; 13536 const dref = new Uint8Array([0x00, 13537 // version 0 13538 0x00, 0x00, 0x00, 13539 // flags 13540 0x00, 0x00, 0x00, 0x01, 13541 // entry_count 13542 0x00, 0x00, 0x00, 0x0c, 13543 // entry_size 13544 0x75, 0x72, 0x6c, 0x20, 13545 // 'url' type 13546 0x00, 13547 // version 0 13548 0x00, 0x00, 0x01 // entry_flags 13549 ]); 13550 const stco = new Uint8Array([0x00, 13551 // version 13552 0x00, 0x00, 0x00, 13553 // flags 13554 0x00, 0x00, 0x00, 0x00 // entry_count 13555 ]); 13556 MP4.STTS = MP4.STSC = MP4.STCO = stco; 13557 MP4.STSZ = new Uint8Array([0x00, 13558 // version 13559 0x00, 0x00, 0x00, 13560 // flags 13561 0x00, 0x00, 0x00, 0x00, 13562 // sample_size 13563 0x00, 0x00, 0x00, 0x00 // sample_count 13564 ]); 13565 MP4.VMHD = new Uint8Array([0x00, 13566 // version 13567 0x00, 0x00, 0x01, 13568 // flags 13569 0x00, 0x00, 13570 // graphicsmode 13571 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 13572 ]); 13573 MP4.SMHD = new Uint8Array([0x00, 13574 // version 13575 0x00, 0x00, 0x00, 13576 // flags 13577 0x00, 0x00, 13578 // balance 13579 0x00, 0x00 // reserved 13580 ]); 13581 MP4.STSD = new Uint8Array([0x00, 13582 // version 0 13583 0x00, 0x00, 0x00, 13584 // flags 13585 0x00, 0x00, 0x00, 0x01]); // entry_count 13586 13587 const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom 13588 const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1 13589 const minorVersion = new Uint8Array([0, 0, 0, 1]); 13590 MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); 13591 MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); 13592 } 13593 static box(type, ...payload) { 13594 let size = 8; 13595 let i = payload.length; 13596 const len = i; 13597 // calculate the total size we need to allocate 13598 while (i--) { 13599 size += payload[i].byteLength; 13600 } 13601 const result = new Uint8Array(size); 13602 result[0] = size >> 24 & 0xff; 13603 result[1] = size >> 16 & 0xff; 13604 result[2] = size >> 8 & 0xff; 13605 result[3] = size & 0xff; 13606 result.set(type, 4); 13607 // copy the payload into the result 13608 for (i = 0, size = 8; i < len; i++) { 13609 // copy payload[i] array @ offset size 13610 result.set(payload[i], size); 13611 size += payload[i].byteLength; 13612 } 13613 return result; 13614 } 13615 static hdlr(type) { 13616 return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); 13617 } 13618 static mdat(data) { 13619 return MP4.box(MP4.types.mdat, data); 13620 } 13621 static mdhd(timescale, duration) { 13622 duration *= timescale; 13623 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 13624 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 13625 return MP4.box(MP4.types.mdhd, new Uint8Array([0x01, 13626 // version 1 13627 0x00, 0x00, 0x00, 13628 // flags 13629 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 13630 // creation_time 13631 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 13632 // modification_time 13633 timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, 13634 // timescale 13635 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x55, 0xc4, 13636 // 'und' language (undetermined) 13637 0x00, 0x00])); 13638 } 13639 static mdia(track) { 13640 return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); 13641 } 13642 static mfhd(sequenceNumber) { 13643 return MP4.box(MP4.types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, 13644 // flags 13645 sequenceNumber >> 24, sequenceNumber >> 16 & 0xff, sequenceNumber >> 8 & 0xff, sequenceNumber & 0xff // sequence_number 13646 ])); 13647 } 13648 static minf(track) { 13649 if (track.type === 'audio') { 13650 return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); 13651 } else { 13652 return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); 13653 } 13654 } 13655 static moof(sn, baseMediaDecodeTime, track) { 13656 return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track, baseMediaDecodeTime)); 13657 } 13658 static moov(tracks) { 13659 let i = tracks.length; 13660 const boxes = []; 13661 while (i--) { 13662 boxes[i] = MP4.trak(tracks[i]); 13663 } 13664 return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(tracks[0].timescale, tracks[0].duration)].concat(boxes).concat(MP4.mvex(tracks))); 13665 } 13666 static mvex(tracks) { 13667 let i = tracks.length; 13668 const boxes = []; 13669 while (i--) { 13670 boxes[i] = MP4.trex(tracks[i]); 13671 } 13672 return MP4.box.apply(null, [MP4.types.mvex, ...boxes]); 13673 } 13674 static mvhd(timescale, duration) { 13675 duration *= timescale; 13676 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 13677 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 13678 const bytes = new Uint8Array([0x01, 13679 // version 1 13680 0x00, 0x00, 0x00, 13681 // flags 13682 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 13683 // creation_time 13684 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 13685 // modification_time 13686 timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, 13687 // timescale 13688 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x01, 0x00, 0x00, 13689 // 1.0 rate 13690 0x01, 0x00, 13691 // 1.0 volume 13692 0x00, 0x00, 13693 // reserved 13694 0x00, 0x00, 0x00, 0x00, 13695 // reserved 13696 0x00, 0x00, 0x00, 0x00, 13697 // reserved 13698 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 13699 // transformation: unity matrix 13700 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13701 // pre_defined 13702 0xff, 0xff, 0xff, 0xff // next_track_ID 13703 ]); 13704 return MP4.box(MP4.types.mvhd, bytes); 13705 } 13706 static sdtp(track) { 13707 const samples = track.samples || []; 13708 const bytes = new Uint8Array(4 + samples.length); 13709 let i; 13710 let flags; 13711 // leave the full box header (4 bytes) all zero 13712 // write the sample table 13713 for (i = 0; i < samples.length; i++) { 13714 flags = samples[i].flags; 13715 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 13716 } 13717 return MP4.box(MP4.types.sdtp, bytes); 13718 } 13719 static stbl(track) { 13720 return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); 13721 } 13722 static avc1(track) { 13723 let sps = []; 13724 let pps = []; 13725 let i; 13726 let data; 13727 let len; 13728 // assemble the SPSs 13729 13730 for (i = 0; i < track.sps.length; i++) { 13731 data = track.sps[i]; 13732 len = data.byteLength; 13733 sps.push(len >>> 8 & 0xff); 13734 sps.push(len & 0xff); 13735 13736 // SPS 13737 sps = sps.concat(Array.prototype.slice.call(data)); 13738 } 13739 13740 // assemble the PPSs 13741 for (i = 0; i < track.pps.length; i++) { 13742 data = track.pps[i]; 13743 len = data.byteLength;
13744 pps.push(len >>> 8 & 0xff); 13745 pps.push(len & 0xff); 13746 pps = pps.concat(Array.prototype.slice.call(data)); 13747 } 13748 const avcc = MP4.box(MP4.types.avcC, new Uint8Array([0x01, 13749 // version 13750 sps[3], 13751 // profile 13752 sps[4], 13753 // profile compat 13754 sps[5], 13755 // level 13756 0xfc | 3, 13757 // lengthSizeMinusOne, hard-coded to 4 bytes 13758 0xe0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets 13759 ].concat(sps).concat([track.pps.length // numOfPictureParameterSets 13760 ]).concat(pps))); // "PPS" 13761 const width = track.width; 13762 const height = track.height; 13763 const hSpacing = track.pixelRatio[0]; 13764 const vSpacing = track.pixelRatio[1]; 13765 return MP4.box(MP4.types.avc1, new Uint8Array([0x00, 0x00, 0x00, 13766 // reserved 13767 0x00, 0x00, 0x00, 13768 // reserved 13769 0x00, 0x01, 13770 // data_reference_index 13771 0x00, 0x00, 13772 // pre_defined 13773 0x00, 0x00, 13774 // reserved 13775 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13776 // pre_defined 13777 width >> 8 & 0xff, width & 0xff, 13778 // width 13779 height >> 8 & 0xff, height & 0xff, 13780 // height 13781 0x00, 0x48, 0x00, 0x00, 13782 // horizresolution 13783 0x00, 0x48, 0x00, 0x00, 13784 // vertresolution 13785 0x00, 0x00, 0x00, 0x00, 13786 // reserved 13787 0x00, 0x01, 13788 // frame_count 13789 0x12, 0x64, 0x61, 0x69, 0x6c, 13790 // dailymotion/hls.js 13791 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13792 // compressorname 13793 0x00, 0x18, 13794 // depth = 24 13795 0x11, 0x11]), 13796 // pre_defined = -1 13797 avcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, 13798 // bufferSizeDB 13799 0x00, 0x2d, 0xc6, 0xc0, 13800 // maxBitrate 13801 0x00, 0x2d, 0xc6, 0xc0])), 13802 // avgBitrate 13803 MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24, 13804 // hSpacing 13805 hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24, 13806 // vSpacing 13807 vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff]))); 13808 } 13809 static esds(track) { 13810 const configlen = track.config.length; 13811 return new Uint8Array([0x00, 13812 // version 0 13813 0x00, 0x00, 0x00, 13814 // flags 13815 13816 0x03, 13817 // descriptor_type 13818 0x17 + configlen, 13819 // length 13820 0x00, 0x01, 13821 // es_id 13822 0x00, 13823 // stream_priority 13824 13825 0x04, 13826 // descriptor_type 13827 0x0f + configlen, 13828 // length 13829 0x40, 13830 // codec : mpeg4_audio 13831 0x15, 13832 // stream_type 13833 0x00, 0x00, 0x00, 13834 // buffer_size 13835 0x00, 0x00, 0x00, 0x00, 13836 // maxBitrate 13837 0x00, 0x00, 0x00, 0x00, 13838 // avgBitrate 13839 13840 0x05 // descriptor_type 13841 ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor 13842 } 13843 static audioStsd(track) { 13844 const samplerate = track.samplerate; 13845 return new Uint8Array([0x00, 0x00, 0x00, 13846 // reserved 13847 0x00, 0x00, 0x00, 13848 // reserved 13849 0x00, 0x01, 13850 // data_reference_index 13851 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13852 // reserved 13853 0x00, track.channelCount, 13854 // channelcount 13855 0x00, 0x10, 13856 // sampleSize:16bits 13857 0x00, 0x00, 0x00, 0x00, 13858 // reserved2 13859 samplerate >> 8 & 0xff, samplerate & 0xff, 13860 // 13861 0x00, 0x00]); 13862 } 13863 static mp4a(track) { 13864 return MP4.box(MP4.types.mp4a, MP4.audioStsd(track), MP4.box(MP4.types.esds, MP4.esds(track))); 13865 } 13866 static mp3(track) { 13867 return MP4.box(MP4.types['.mp3'], MP4.audioStsd(track)); 13868 } 13869 static ac3(track) { 13870 return MP4.box(MP4.types['ac-3'], MP4.audioStsd(track), MP4.box(MP4.types.dac3, track.config)); 13871 } 13872 static stsd(track) { 13873 if (track.type === 'audio') { 13874 if (track.segmentCodec === 'mp3' && track.codec === 'mp3') { 13875 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(track)); 13876 } 13877 if (track.segmentCodec === 'ac3') { 13878 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track)); 13879 } 13880 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); 13881 } else { 13882 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); 13883 } 13884 } 13885 static tkhd(track) { 13886 const id = track.id; 13887 const duration = track.duration * track.timescale; 13888 const width = track.width; 13889 const height = track.height; 13890 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 13891 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 13892 return MP4.box(MP4.types.tkhd, new Uint8Array([0x01, 13893 // version 1 13894 0x00, 0x00, 0x07, 13895 // flags 13896 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 13897 // creation_time 13898 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 13899 // modification_time 13900 id >> 24 & 0xff, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, 13901 // track_ID 13902 0x00, 0x00, 0x00, 0x00, 13903 // reserved 13904 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13905 // reserved 13906 0x00, 0x00, 13907 // layer 13908 0x00, 0x00, 13909 // alternate_group 13910 0x00, 0x00, 13911 // non-audio track volume 13912 0x00, 0x00, 13913 // reserved 13914 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 13915 // transformation: unity matrix 13916 width >> 8 & 0xff, width & 0xff, 0x00, 0x00, 13917 // width 13918 height >> 8 & 0xff, height & 0xff, 0x00, 0x00 // height 13919 ])); 13920 } 13921 static traf(track, baseMediaDecodeTime) { 13922 const sampleDependencyTable = MP4.sdtp(track); 13923 const id = track.id; 13924 const upperWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime / (UINT32_MAX + 1)); 13925 const lowerWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime % (UINT32_MAX + 1)); 13926 return MP4.box(MP4.types.traf, MP4.box(MP4.types.tfhd, new Uint8Array([0x00, 13927 // version 0 13928 0x00, 0x00, 0x00, 13929 // flags 13930 id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff // track_ID 13931 ])), MP4.box(MP4.types.tfdt, new Uint8Array([0x01, 13932 // version 1 13933 0x00, 0x00, 0x00, 13934 // flags 13935 upperWordBaseMediaDecodeTime >> 24, upperWordBaseMediaDecodeTime >> 16 & 0xff, upperWordBaseMediaDecodeTime >> 8 & 0xff, upperWordBaseMediaDecodeTime & 0xff, lowerWordBaseMediaDecodeTime >> 24, lowerWordBaseMediaDecodeTime >> 16 & 0xff, lowerWordBaseMediaDecodeTime >> 8 & 0xff, lowerWordBaseMediaDecodeTime & 0xff])), MP4.trun(track, sampleDependencyTable.length + 16 + 13936 // tfhd 13937 20 + 13938 // tfdt 13939 8 + 13940 // traf header 13941 16 + 13942 // mfhd 13943 8 + 13944 // moof header 13945 8), 13946 // mdat header 13947 sampleDependencyTable); 13948 } 13949 13950 /** 13951 * Generate a track box. 13952 * @param track a track definition 13953 */ 13954 static trak(track) { 13955 track.duration = track.duration || 0xffffffff; 13956 return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); 13957 } 13958 static trex(track) { 13959 const id = track.id; 13960 return MP4.box(MP4.types.trex, new Uint8Array([0x00, 13961 // version 0 13962 0x00, 0x00, 0x00, 13963 // flags 13964 id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, 13965 // track_ID 13966 0x00, 0x00, 0x00, 0x01, 13967 // default_sample_description_index 13968 0x00, 0x00, 0x00, 0x00, 13969 // default_sample_duration 13970 0x00, 0x00, 0x00, 0x00, 13971 // default_sample_size 13972 0x00, 0x01, 0x00, 0x01 // default_sample_flags 13973 ])); 13974 } 13975 static trun(track, offset) { 13976 const samples = track.samples || [];
13977 const len = samples.length; 13978 const arraylen = 12 + 16 * len; 13979 const array = new Uint8Array(arraylen); 13980 let i; 13981 let sample; 13982 let duration; 13983 let size; 13984 let flags; 13985 let cts; 13986 offset += 8 + arraylen; 13987 array.set([track.type === 'video' ? 0x01 : 0x00, 13988 // version 1 for video with signed-int sample_composition_time_offset 13989 0x00, 0x0f, 0x01, 13990 // flags 13991 len >>> 24 & 0xff, len >>> 16 & 0xff, len >>> 8 & 0xff, len & 0xff, 13992 // sample_count 13993 offset >>> 24 & 0xff, offset >>> 16 & 0xff, offset >>> 8 & 0xff, offset & 0xff // data_offset 13994 ], 0); 13995 for (i = 0; i < len; i++) { 13996 sample = samples[i]; 13997 duration = sample.duration; 13998 size = sample.size; 13999 flags = sample.flags; 14000 cts = sample.cts; 14001 array.set([duration >>> 24 & 0xff, duration >>> 16 & 0xff, duration >>> 8 & 0xff, duration & 0xff, 14002 // sample_duration 14003 size >>> 24 & 0xff, size >>> 16 & 0xff, size >>> 8 & 0xff, size & 0xff, 14004 // sample_size 14005 flags.isLeading << 2 | flags.dependsOn, flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.paddingValue << 1 | flags.isNonSync, flags.degradPrio & 0xf0 << 8, flags.degradPrio & 0x0f, 14006 // sample_flags 14007 cts >>> 24 & 0xff, cts >>> 16 & 0xff, cts >>> 8 & 0xff, cts & 0xff // sample_composition_time_offset 14008 ], 12 + 16 * i); 14009 } 14010 return MP4.box(MP4.types.trun, array); 14011 } 14012 static initSegment(tracks) { 14013 if (!MP4.types) { 14014 MP4.init(); 14015 } 14016 const movie = MP4.moov(tracks); 14017 const result = appendUint8Array(MP4.FTYP, movie); 14018 return result; 14019 } 14020} 14021MP4.types = void 0; 14022MP4.HDLR_TYPES = void 0; 14023MP4.STTS = void 0; 14024MP4.STSC = void 0; 14025MP4.STCO = void 0; 14026MP4.STSZ = void 0; 14027MP4.VMHD = void 0; 14028MP4.SMHD = void 0; 14029MP4.STSD = void 0; 14030MP4.FTYP = void 0; 14031MP4.DINF = void 0; 14032 14033const MPEG_TS_CLOCK_FREQ_HZ = 90000; 14034function toTimescaleFromBase(baseTime, destScale, srcBase = 1, round = false) { 14035 const result = baseTime * destScale * srcBase; // equivalent to `(value * scale) / (1 / base)` 14036 return round ? Math.round(result) : result; 14037} 14038function toTimescaleFromScale(baseTime, destScale, srcScale = 1, round = false) { 14039 return toTimescaleFromBase(baseTime, destScale, 1 / srcScale, round); 14040} 14041function toMsFromMpegTsClock(baseTime, round = false) { 14042 return toTimescaleFromBase(baseTime, 1000, 1 / MPEG_TS_CLOCK_FREQ_HZ, round); 14043} 14044function toMpegTsClockFromTimescale(baseTime, srcScale = 1) { 14045 return toTimescaleFromBase(baseTime, MPEG_TS_CLOCK_FREQ_HZ, 1 / srcScale); 14046} 14047 14048const MAX_SILENT_FRAME_DURATION = 10 * 1000; // 10 seconds 14049const AAC_SAMPLES_PER_FRAME = 1024; 14050const MPEG_AUDIO_SAMPLE_PER_FRAME = 1152; 14051const AC3_SAMPLES_PER_FRAME = 1536; 14052let chromeVersion = null; 14053let safariWebkitVersion = null; 14054class MP4Remuxer { 14055 constructor(observer, config, typeSupported, vendor = '') { 14056 this.observer = void 0; 14057 this.config = void 0; 14058 this.typeSupported = void 0; 14059 this.ISGenerated = false; 14060 this._initPTS = null; 14061 this._initDTS = null; 14062 this.nextAvcDts = null; 14063 this.nextAudioPts = null; 14064 this.videoSampleDuration = null; 14065 this.isAudioContiguous = false; 14066 this.isVideoContiguous = false; 14067 this.videoTrackConfig = void 0; 14068 this.observer = observer; 14069 this.config = config; 14070 this.typeSupported = typeSupported; 14071 this.ISGenerated = false; 14072 if (chromeVersion === null) { 14073 const userAgent = navigator.userAgent || ''; 14074 const result = userAgent.match(/Chrome\/(\d+)/i); 14075 chromeVersion = result ? parseInt(result[1]) : 0; 14076 } 14077 if (safariWebkitVersion === null) { 14078 const result = navigator.userAgent.match(/Safari\/(\d+)/i); 14079 safariWebkitVersion = result ? parseInt(result[1]) : 0; 14080 } 14081 } 14082 destroy() { 14083 // @ts-ignore 14084 this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; 14085 } 14086 resetTimeStamp(defaultTimeStamp) { 14087 logger.log('[mp4-remuxer]: initPTS & initDTS reset'); 14088 this._initPTS = this._initDTS = defaultTimeStamp; 14089 } 14090 resetNextTimestamp() { 14091 logger.log('[mp4-remuxer]: reset next timestamp'); 14092 this.isVideoContiguous = false; 14093 this.isAudioContiguous = false;
14094 } 14095 resetInitSegment() { 14096 logger.log('[mp4-remuxer]: ISGenerated flag reset'); 14097 this.ISGenerated = false; 14098 this.videoTrackConfig = undefined; 14099 } 14100 getVideoStartPts(videoSamples) { 14101 let rolloverDetected = false; 14102 const startPTS = videoSamples.reduce((minPTS, sample) => { 14103 const delta = sample.pts - minPTS; 14104 if (delta < -4294967296) { 14105 // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation 14106 rolloverDetected = true; 14107 return normalizePts(minPTS, sample.pts); 14108 } else if (delta > 0) { 14109 return minPTS; 14110 } else { 14111 return sample.pts; 14112 } 14113 }, videoSamples[0].pts); 14114 if (rolloverDetected) { 14115 logger.debug('PTS rollover detected'); 14116 } 14117 return startPTS; 14118 } 14119 remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, flush, playlistType) { 14120 let video; 14121 let audio; 14122 let initSegment; 14123 let text; 14124 let id3; 14125 let independent; 14126 let audioTimeOffset = timeOffset; 14127 let videoTimeOffset = timeOffset; 14128 14129 // If we're remuxing audio and video progressively, wait until we've received enough samples for each track before proceeding. 14130 // This is done to synchronize the audio and video streams. We know if the current segment will have samples if the "pid" 14131 // parameter is greater than -1. The pid is set when the PMT is parsed, which contains the tracks list. 14132 // However, if the initSegment has already been generated, or we've reached the end of a segment (flush), 14133 // then we can remux one track without waiting for the other. 14134 const hasAudio = audioTrack.pid > -1; 14135 const hasVideo = videoTrack.pid > -1; 14136 const length = videoTrack.samples.length; 14137 const enoughAudioSamples = audioTrack.samples.length > 0; 14138 const enoughVideoSamples = flush && length > 0 || length > 1; 14139 const canRemuxAvc = (!hasAudio || enoughAudioSamples) && (!hasVideo || enoughVideoSamples) || this.ISGenerated || flush; 14140 if (canRemuxAvc) { 14141 if (this.ISGenerated) { 14142 var _videoTrack$pixelRati, _config$pixelRatio, _videoTrack$pixelRati2, _config$pixelRatio2; 14143 const config = this.videoTrackConfig; 14144 if (config && (videoTrack.width !== config.width || videoTrack.height !== config.height || ((_videoTrack$pixelRati = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati[0]) !== ((_config$pixelRatio = config.pixelRatio) == null ? void 0 : _config$pixelRatio[0]) || ((_videoTrack$pixelRati2 = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati2[1]) !== ((_config$pixelRatio2 = config.pixelRatio) == null ? void 0 : _config$pixelRatio2[1]))) { 14145 this.resetInitSegment(); 14146 } 14147 } else { 14148 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 14149 } 14150 const isVideoContiguous = this.isVideoContiguous; 14151 let firstKeyFrameIndex = -1; 14152 let firstKeyFramePTS; 14153 if (enoughVideoSamples) { 14154 firstKeyFrameIndex = findKeyframeIndex(videoTrack.samples); 14155 if (!isVideoContiguous && this.config.forceKeyFrameOnDiscontinuity) { 14156 independent = true; 14157 if (firstKeyFrameIndex > 0) { 14158 logger.warn(`[mp4-remuxer]: Dropped ${firstKeyFrameIndex} out of ${length} video samples due to a missing keyframe`); 14159 const startPTS = this.getVideoStartPts(videoTrack.samples); 14160 videoTrack.samples = videoTrack.samples.slice(firstKeyFrameIndex); 14161 videoTrack.dropped += firstKeyFrameIndex; 14162 videoTimeOffset += (videoTrack.samples[0].pts - startPTS) / videoTrack.inputTimeScale; 14163 firstKeyFramePTS = videoTimeOffset; 14164 } else if (firstKeyFrameIndex === -1) { 14165 logger.warn(`[mp4-remuxer]: No keyframe found out of ${length} video samples`); 14166 independent = false; 14167 } 14168 } 14169 } 14170 if (this.ISGenerated) { 14171 if (enoughAudioSamples && enoughVideoSamples) { 14172 // timeOffset is expected to be the offset of the first timestamp of this fragment (first DTS) 14173 // if first audio DTS is not aligned with first video DTS then we need to take that into account 14174 // when providing timeOffset to remuxAudio / remuxVideo. if we don't do that, there might be a permanent / small 14175 // drift between audio and video streams 14176 const startPTS = this.getVideoStartPts(videoTrack.samples); 14177 const tsDelta = normalizePts(audioTrack.samples[0].pts, startPTS) - startPTS; 14178 const audiovideoTimestampDelta = tsDelta / videoTrack.inputTimeScale; 14179 audioTimeOffset += Math.max(0, audiovideoTimestampDelta); 14180 videoTimeOffset += Math.max(0, -audiovideoTimestampDelta); 14181 } 14182 14183 // Purposefully remuxing audio before video, so that remuxVideo can use nextAudioPts, which is calculated in remuxAudio. 14184 if (enoughAudioSamples) {
vendor: 4,806 bytes, lines 14185-14301
14185 // if initSegment was generated without audio samples, regenerate it again 14186 if (!audioTrack.samplerate) { 14187 logger.warn('[mp4-remuxer]: regenerate InitSegment as audio detected'); 14188 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 14189 } 14190 audio = this.remuxAudio(audioTrack, audioTimeOffset, this.isAudioContiguous, accurateTimeOffset, hasVideo || enoughVideoSamples || playlistType === PlaylistLevelType.AUDIO ? videoTimeOffset : undefined); 14191 if (enoughVideoSamples) { 14192 const audioTrackLength = audio ? audio.endPTS - audio.startPTS : 0; 14193 // if initSegment was generated without video samples, regenerate it again 14194 if (!videoTrack.inputTimeScale) { 14195 logger.warn('[mp4-remuxer]: regenerate InitSegment as video detected'); 14196 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 14197 } 14198 video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, audioTrackLength); 14199 } 14200 } else if (enoughVideoSamples) { 14201 video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, 0); 14202 } 14203 if (video) { 14204 video.firstKeyFrame = firstKeyFrameIndex; 14205 video.independent = firstKeyFrameIndex !== -1; 14206 video.firstKeyFramePTS = firstKeyFramePTS; 14207 } 14208 } 14209 } 14210 14211 // Allow ID3 and text to remux, even if more audio/video samples are required 14212 if (this.ISGenerated && this._initPTS && this._initDTS) { 14213 if (id3Track.samples.length) { 14214 id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, this._initPTS, this._initDTS); 14215 } 14216 if (textTrack.samples.length) { 14217 text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, this._initPTS); 14218 } 14219 } 14220 return { 14221 audio, 14222 video, 14223 initSegment, 14224 independent, 14225 text, 14226 id3 14227 }; 14228 } 14229 generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset) { 14230 const audioSamples = audioTrack.samples; 14231 const videoSamples = videoTrack.samples; 14232 const typeSupported = this.typeSupported; 14233 const tracks = {}; 14234 const _initPTS = this._initPTS; 14235 let computePTSDTS = !_initPTS || accurateTimeOffset; 14236 let container = 'audio/mp4'; 14237 let initPTS; 14238 let initDTS; 14239 let timescale; 14240 if (computePTSDTS) { 14241 initPTS = initDTS = Infinity; 14242 } 14243 if (audioTrack.config && audioSamples.length) { 14244 // let's use audio sampling rate as MP4 time scale. 14245 // rationale is that there is a integer nb of audio frames per audio sample (1024 for AAC) 14246 // using audio sampling rate here helps having an integer MP4 frame duration 14247 // this avoids potential rounding issue and AV sync issue 14248 audioTrack.timescale = audioTrack.samplerate; 14249 switch (audioTrack.segmentCodec) { 14250 case 'mp3': 14251 if (typeSupported.mpeg) { 14252 // Chrome and Safari 14253 container = 'audio/mpeg'; 14254 audioTrack.codec = ''; 14255 } else if (typeSupported.mp3) { 14256 // Firefox 14257 audioTrack.codec = 'mp3'; 14258 } 14259 break; 14260 case 'ac3': 14261 audioTrack.codec = 'ac-3'; 14262 break; 14263 } 14264 tracks.audio = { 14265 id: 'audio', 14266 container: container, 14267 codec: audioTrack.codec, 14268 initSegment: audioTrack.segmentCodec === 'mp3' && typeSupported.mpeg ? new Uint8Array(0) : MP4.initSegment([audioTrack]), 14269 metadata: { 14270 channelCount: audioTrack.channelCount 14271 } 14272 }; 14273 if (computePTSDTS) { 14274 timescale = audioTrack.inputTimeScale; 14275 if (!_initPTS || timescale !== _initPTS.timescale) { 14276 // remember first PTS of this demuxing context. for audio, PTS = DTS 14277 initPTS = initDTS = audioSamples[0].pts - Math.round(timescale * timeOffset); 14278 } else { 14279 computePTSDTS = false; 14280 } 14281 } 14282 } 14283 if (videoTrack.sps && videoTrack.pps && videoSamples.length) { 14284 // let's use input time scale as MP4 video timescale 14285 // we use input time scale straight away to avoid rounding issues on frame duration / cts computation 14286 videoTrack.timescale = videoTrack.inputTimeScale; 14287 tracks.video = { 14288 id: 'main', 14289 container: 'video/mp4', 14290 codec: videoTrack.codec, 14291 initSegment: MP4.initSegment([videoTrack]), 14292 metadata: { 14293 width: videoTrack.width, 14294 height: videoTrack.height 14295 } 14296 }; 14297 if (computePTSDTS) { 14298 timescale = videoTrack.inputTimeScale; 14299 if (!_initPTS || timescale !== _initPTS.timescale) { 14300 const startPTS = this.getVideoStartPts(videoSamples); 14301 const startOffset = Math.round(timescale * timeOffset);
14302 initDTS = Math.min(initDTS, normalizePts(videoSamples[0].dts, startPTS) - startOffset); 14303 initPTS = Math.min(initPTS, startPTS - startOffset); 14304 } else { 14305 computePTSDTS = false; 14306 } 14307 } 14308 this.videoTrackConfig = { 14309 width: videoTrack.width, 14310 height: videoTrack.height, 14311 pixelRatio: videoTrack.pixelRatio 14312 }; 14313 } 14314 if (Object.keys(tracks).length) { 14315 this.ISGenerated = true; 14316 if (computePTSDTS) { 14317 this._initPTS = { 14318 baseTime: initPTS, 14319 timescale: timescale 14320 }; 14321 this._initDTS = { 14322 baseTime: initDTS, 14323 timescale: timescale 14324 }; 14325 } else { 14326 initPTS = timescale = undefined; 14327 } 14328 return { 14329 tracks, 14330 initPTS, 14331 timescale 14332 }; 14333 } 14334 } 14335 remuxVideo(track, timeOffset, contiguous, audioTrackLength) { 14336 const timeScale = track.inputTimeScale; 14337 const inputSamples = track.samples; 14338 const outputSamples = []; 14339 const nbSamples = inputSamples.length; 14340 const initPTS = this._initPTS; 14341 let nextAvcDts = this.nextAvcDts; 14342 let offset = 8; 14343 let mp4SampleDuration = this.videoSampleDuration; 14344 let firstDTS; 14345 let lastDTS; 14346 let minPTS = Number.POSITIVE_INFINITY; 14347 let maxPTS = Number.NEGATIVE_INFINITY; 14348 let sortSamples = false; 14349 14350 // if parsed fragment is contiguous with last one, let's use last DTS value as reference 14351 if (!contiguous || nextAvcDts === null) { 14352 const pts = timeOffset * timeScale; 14353 const cts = inputSamples[0].pts - normalizePts(inputSamples[0].dts, inputSamples[0].pts); 14354 if (chromeVersion && nextAvcDts !== null && Math.abs(pts - cts - nextAvcDts) < 15000) { 14355 // treat as contigous to adjust samples that would otherwise produce video buffer gaps in Chrome 14356 contiguous = true; 14357 } else { 14358 // if not contiguous, let's use target timeOffset 14359 nextAvcDts = pts - cts; 14360 } 14361 } 14362 14363 // PTS is coded on 33bits, and can loop from -2^32 to 2^32 14364 // PTSNormalize will make PTS/DTS value monotonic, we use last known DTS value as reference value 14365 const initTime = initPTS.baseTime * timeScale / initPTS.timescale; 14366 for (let i = 0; i < nbSamples; i++) { 14367 const sample = inputSamples[i]; 14368 sample.pts = normalizePts(sample.pts - initTime, nextAvcDts); 14369 sample.dts = normalizePts(sample.dts - initTime, nextAvcDts); 14370 if (sample.dts < inputSamples[i > 0 ? i - 1 : i].dts) { 14371 sortSamples = true; 14372 } 14373 } 14374 14375 // sort video samples by DTS then PTS then demux id order 14376 if (sortSamples) { 14377 inputSamples.sort(function (a, b) { 14378 const deltadts = a.dts - b.dts; 14379 const deltapts = a.pts - b.pts; 14380 return deltadts || deltapts; 14381 }); 14382 } 14383 14384 // Get first/last DTS 14385 firstDTS = inputSamples[0].dts; 14386 lastDTS = inputSamples[inputSamples.length - 1].dts; 14387 14388 // Sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS 14389 // set this constant duration as being the avg delta between consecutive DTS. 14390 const inputDuration = lastDTS - firstDTS; 14391 const averageSampleDuration = inputDuration ? Math.round(inputDuration / (nbSamples - 1)) : mp4SampleDuration || track.inputTimeScale / 30; 14392 14393 // if fragment are contiguous, detect hole/overlapping between fragments 14394 if (contiguous) { 14395 // check timestamp continuity across consecutive fragments (this is to remove inter-fragment gap/hole) 14396 const delta = firstDTS - nextAvcDts; 14397 const foundHole = delta > averageSampleDuration; 14398 const foundOverlap = delta < -1; 14399 if (foundHole || foundOverlap) { 14400 if (foundHole) { 14401 logger.warn(`AVC: ${toMsFromMpegTsClock(delta, true)} ms (${delta}dts) hole between fragments detected at ${timeOffset.toFixed(3)}`); 14402 } else { 14403 logger.warn(`AVC: ${toMsFromMpegTsClock(-delta, true)} ms (${delta}dts) overlapping between fragments detected at ${timeOffset.toFixed(3)}`); 14404 } 14405 if (!foundOverlap || nextAvcDts >= inputSamples[0].pts || chromeVersion) { 14406 firstDTS = nextAvcDts; 14407 const firstPTS = inputSamples[0].pts - delta; 14408 if (foundHole) { 14409 inputSamples[0].dts = firstDTS; 14410 inputSamples[0].pts = firstPTS; 14411 } else { 14412 for (let i = 0; i < inputSamples.length; i++) { 14413 if (inputSamples[i].dts > firstPTS) { 14414 break; 14415 } 14416 inputSamples[i].dts -= delta; 14417 inputSamples[i].pts -= delta; 14418 } 14419 } 14420 logger.log(`Video: Initial PTS/DTS adjusted: ${toMsFromMpegTsClock(firstPTS, true)}/${toMsFromMpegTsClock(firstDTS, true)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`); 14421 } 14422 } 14423 } 14424 firstDTS = Math.max(0, firstDTS); 14425 let nbNalu = 0; 14426 let naluLen = 0; 14427 let dtsStep = firstDTS; 14428 for (let i = 0; i < nbSamples; i++) { 14429 // compute total/avc sample length and nb of NAL units 14430 const sample = inputSamples[i];
vendor: 5,197 bytes, lines 14431-14545
14431 const units = sample.units; 14432 const nbUnits = units.length; 14433 let sampleLen = 0; 14434 for (let j = 0; j < nbUnits; j++) { 14435 sampleLen += units[j].data.length; 14436 } 14437 naluLen += sampleLen; 14438 nbNalu += nbUnits; 14439 sample.length = sampleLen; 14440 14441 // ensure sample monotonic DTS 14442 if (sample.dts < dtsStep) { 14443 sample.dts = dtsStep; 14444 dtsStep += averageSampleDuration / 4 | 0 || 1; 14445 } else { 14446 dtsStep = sample.dts; 14447 } 14448 minPTS = Math.min(sample.pts, minPTS); 14449 maxPTS = Math.max(sample.pts, maxPTS); 14450 } 14451 lastDTS = inputSamples[nbSamples - 1].dts; 14452 14453 /* concatenate the video data and construct the mdat in place 14454 (need 8 more bytes to fill length and mpdat type) */ 14455 const mdatSize = naluLen + 4 * nbNalu + 8; 14456 let mdat; 14457 try { 14458 mdat = new Uint8Array(mdatSize); 14459 } catch (err) { 14460 this.observer.emit(Events.ERROR, Events.ERROR, { 14461 type: ErrorTypes.MUX_ERROR, 14462 details: ErrorDetails.REMUX_ALLOC_ERROR, 14463 fatal: false, 14464 error: err, 14465 bytes: mdatSize, 14466 reason: `fail allocating video mdat ${mdatSize}` 14467 }); 14468 return; 14469 } 14470 const view = new DataView(mdat.buffer); 14471 view.setUint32(0, mdatSize); 14472 mdat.set(MP4.types.mdat, 4); 14473 let stretchedLastFrame = false; 14474 let minDtsDelta = Number.POSITIVE_INFINITY; 14475 let minPtsDelta = Number.POSITIVE_INFINITY; 14476 let maxDtsDelta = Number.NEGATIVE_INFINITY; 14477 let maxPtsDelta = Number.NEGATIVE_INFINITY; 14478 for (let i = 0; i < nbSamples; i++) { 14479 const VideoSample = inputSamples[i]; 14480 const VideoSampleUnits = VideoSample.units; 14481 let mp4SampleLength = 0; 14482 // convert NALU bitstream to MP4 format (prepend NALU with size field) 14483 for (let j = 0, nbUnits = VideoSampleUnits.length; j < nbUnits; j++) { 14484 const unit = VideoSampleUnits[j]; 14485 const unitData = unit.data; 14486 const unitDataLen = unit.data.byteLength; 14487 view.setUint32(offset, unitDataLen); 14488 offset += 4; 14489 mdat.set(unitData, offset); 14490 offset += unitDataLen; 14491 mp4SampleLength += 4 + unitDataLen; 14492 } 14493 14494 // expected sample duration is the Decoding Timestamp diff of consecutive samples 14495 let ptsDelta; 14496 if (i < nbSamples - 1) { 14497 mp4SampleDuration = inputSamples[i + 1].dts - VideoSample.dts; 14498 ptsDelta = inputSamples[i + 1].pts - VideoSample.pts; 14499 } else { 14500 const config = this.config; 14501 const lastFrameDuration = i > 0 ? VideoSample.dts - inputSamples[i - 1].dts : averageSampleDuration; 14502 ptsDelta = i > 0 ? VideoSample.pts - inputSamples[i - 1].pts : averageSampleDuration; 14503 if (config.stretchShortVideoTrack && this.nextAudioPts !== null) { 14504 // In some cases, a segment's audio track duration may exceed the video track duration. 14505 // Since we've already remuxed audio, and we know how long the audio track is, we look to 14506 // see if the delta to the next segment is longer than maxBufferHole. 14507 // If so, playback would potentially get stuck, so we artificially inflate 14508 // the duration of the last frame to minimize any potential gap between segments. 14509 const gapTolerance = Math.floor(config.maxBufferHole * timeScale); 14510 const deltaToFrameEnd = (audioTrackLength ? minPTS + audioTrackLength * timeScale : this.nextAudioPts) - VideoSample.pts; 14511 if (deltaToFrameEnd > gapTolerance) { 14512 // We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video 14513 // frame overlap. maxBufferHole should be >> lastFrameDuration anyway. 14514 mp4SampleDuration = deltaToFrameEnd - lastFrameDuration; 14515 if (mp4SampleDuration < 0) { 14516 mp4SampleDuration = lastFrameDuration; 14517 } else { 14518 stretchedLastFrame = true; 14519 } 14520 logger.log(`[mp4-remuxer]: It is approximately ${deltaToFrameEnd / 90} ms to the next segment; using duration ${mp4SampleDuration / 90} ms for the last video frame.`); 14521 } else { 14522 mp4SampleDuration = lastFrameDuration; 14523 } 14524 } else { 14525 mp4SampleDuration = lastFrameDuration; 14526 } 14527 } 14528 const compositionTimeOffset = Math.round(VideoSample.pts - VideoSample.dts); 14529 minDtsDelta = Math.min(minDtsDelta, mp4SampleDuration); 14530 maxDtsDelta = Math.max(maxDtsDelta, mp4SampleDuration); 14531 minPtsDelta = Math.min(minPtsDelta, ptsDelta); 14532 maxPtsDelta = Math.max(maxPtsDelta, ptsDelta); 14533 outputSamples.push(new Mp4Sample(VideoSample.key, mp4SampleDuration, mp4SampleLength, compositionTimeOffset)); 14534 } 14535 if (outputSamples.length) { 14536 if (chromeVersion) { 14537 if (chromeVersion < 70) { 14538 // Chrome workaround, mark first sample as being a Random Access Point (keyframe) to avoid sourcebuffer append issue 14539 // https://code.google.com/p/chromium/issues/detail?id=229412 14540 const flags = outputSamples[0].flags; 14541 flags.dependsOn = 2; 14542 flags.isNonSync = 0; 14543 } 14544 } else if (safariWebkitVersion) { 14545 // Fix for "CNN special report, with CC" in test-streams (Safari browser only
14545) 14546 // Ignore DTS when frame durations are irregular. Safari MSE does not handle this leading to gaps. 14547 if (maxPtsDelta - minPtsDelta < maxDtsDelta - minDtsDelta && averageSampleDuration / maxDtsDelta < 0.025 && outputSamples[0].cts === 0) { 14548 logger.warn('Found irregular gaps in sample duration. Using PTS instead of DTS to determine MP4 sample duration.'); 14549 let dts = firstDTS; 14550 for (let i = 0, len = outputSamples.length; i < len; i++) { 14551 const nextDts = dts + outputSamples[i].duration; 14552 const pts = dts + outputSamples[i].cts; 14553 if (i < len - 1) { 14554 const nextPts = nextDts + outputSamples[i + 1].cts; 14555 outputSamples[i].duration = nextPts - pts; 14556 } else { 14557 outputSamples[i].duration = i ? outputSamples[i - 1].duration : averageSampleDuration; 14558 } 14559 outputSamples[i].cts = 0; 14560 dts = nextDts; 14561 } 14562 } 14563 } 14564 } 14565 // next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale) 14566 mp4SampleDuration = stretchedLastFrame || !mp4SampleDuration ? averageSampleDuration : mp4SampleDuration; 14567 this.nextAvcDts = nextAvcDts = lastDTS + mp4SampleDuration; 14568 this.videoSampleDuration = mp4SampleDuration; 14569 this.isVideoContiguous = true; 14570 const moof = MP4.moof(track.sequenceNumber++, firstDTS, _extends({}, track, { 14571 samples: outputSamples 14572 })); 14573 const type = 'video'; 14574 const data = { 14575 data1: moof, 14576 data2: mdat, 14577 startPTS: minPTS / timeScale, 14578 endPTS: (maxPTS + mp4SampleDuration) / timeScale, 14579 startDTS: firstDTS / timeScale, 14580 endDTS: nextAvcDts / timeScale, 14581 type, 14582 hasAudio: false, 14583 hasVideo: true, 14584 nb: outputSamples.length, 14585 dropped: track.dropped 14586 }; 14587 track.samples = []; 14588 track.dropped = 0; 14589 return data; 14590 } 14591 getSamplesPerFrame(track) { 14592 switch (track.segmentCodec) { 14593 case 'mp3': 14594 return MPEG_AUDIO_SAMPLE_PER_FRAME; 14595 case 'ac3': 14596 return AC3_SAMPLES_PER_FRAME; 14597 default: 14598 return AAC_SAMPLES_PER_FRAME; 14599 } 14600 } 14601 remuxAudio(track, timeOffset, contiguous, accurateTimeOffset, videoTimeOffset) { 14602 const inputTimeScale = track.inputTimeScale; 14603 const mp4timeScale = track.samplerate ? track.samplerate : inputTimeScale; 14604 const scaleFactor = inputTimeScale / mp4timeScale; 14605 const mp4SampleDuration = this.getSamplesPerFrame(track); 14606 const inputSampleDuration = mp4SampleDuration * scaleFactor; 14607 const initPTS = this._initPTS; 14608 const rawMPEG = track.segmentCodec === 'mp3' && this.typeSupported.mpeg; 14609 const outputSamples = []; 14610 const alignedWithVideo = videoTimeOffset !== undefined; 14611 let inputSamples = track.samples; 14612 let offset = rawMPEG ? 0 : 8; 14613 let nextAudioPts = this.nextAudioPts || -1; 14614 14615 // window.audioSamples ? window.audioSamples.push(inputSamples.map(s => s.pts)) : (window.audioSamples = [inputSamples.map(s => s.pts)]); 14616 14617 // for audio samples, also consider consecutive fragments as being contiguous (even if a level switch occurs), 14618 // for sake of clarity: 14619 // consecutive fragments are frags with 14620 // - less than 100ms gaps between new time offset (if accurate) and next expected PTS OR 14621 // - less than 20 audio frames distance 14622 // contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1) 14623 // this helps ensuring audio continuity 14624 // and this also avoids audio glitches/cut when switching quality, or reporting wrong duration on first audio frame 14625 const timeOffsetMpegTS = timeOffset * inputTimeScale; 14626 const initTime = initPTS.baseTime * inputTimeScale / initPTS.timescale; 14627 this.isAudioContiguous = contiguous = contiguous || inputSamples.length && nextAudioPts > 0 && (accurateTimeOffset && Math.abs(timeOffsetMpegTS - nextAudioPts) < 9000 || Math.abs(normalizePts(inputSamples[0].pts - initTime, timeOffsetMpegTS) - nextAudioPts) < 20 * inputSampleDuration); 14628 14629 // compute normalized PTS
14630 inputSamples.forEach(function (sample) { 14631 sample.pts = normalizePts(sample.pts - initTime, timeOffsetMpegTS); 14632 }); 14633 if (!contiguous || nextAudioPts < 0) { 14634 // filter out sample with negative PTS that are not playable anyway 14635 // if we don't remove these negative samples, they will shift all audio samples forward. 14636 // leading to audio overlap between current / next fragment 14637 inputSamples = inputSamples.filter(sample => sample.pts >= 0); 14638 14639 // in case all samples have negative PTS, and have been filtered out, return now 14640 if (!inputSamples.length) { 14641 return; 14642 } 14643 if (videoTimeOffset === 0) { 14644 // Set the start to 0 to match video so that start gaps larger than inputSampleDuration are filled with silence 14645 nextAudioPts = 0; 14646 } else if (accurateTimeOffset && !alignedWithVideo) { 14647 // When not seeking, not live, and LevelDetails.PTSKnown, use fragment start as predicted next audio PTS 14648 nextAudioPts = Math.max(0, timeOffsetMpegTS); 14649 } else { 14650 // if frags are not contiguous and if we cant trust time offset, let's use first sample PTS as next audio PTS 14651 nextAudioPts = inputSamples[0].pts; 14652 } 14653 } 14654 14655 // If the audio track is missing samples, the frames seem to get "left-shifted" within the 14656 // resulting mp4 segment, causing sync issues and leaving gaps at the end of the audio segment. 14657 // In an effort to prevent this from happening, we inject frames here where there are gaps. 14658 // When possible, we inject a silent frame; when that's not possible, we duplicate the last 14659 // frame. 14660 14661 if (track.segmentCodec === 'aac') { 14662 const maxAudioFramesDrift = this.config.maxAudioFramesDrift; 14663 for (let i = 0, nextPts = nextAudioPts; i < inputSamples.length; i++) { 14664 // First, let's see how far off this frame is from where we expect it to be 14665 const sample = inputSamples[i]; 14666 const pts = sample.pts; 14667 const delta = pts - nextPts; 14668 const duration = Math.abs(1000 * delta / inputTimeScale); 14669 14670 // When remuxing with video, if we're overlapping by more than a duration, drop this sample to stay in sync 14671 if (delta <= -maxAudioFramesDrift * inputSampleDuration && alignedWithVideo) { 14672 if (i === 0) { 14673 logger.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps nextAudioPts by ${Math.round(1000 * delta / inputTimeScale)} ms.`); 14674 this.nextAudioPts = nextAudioPts = nextPts = pts; 14675 } 14676 } // eslint-disable-line brace-style 14677 14678 // Insert missing frames if: 14679 // 1: We're more than maxAudioFramesDrift frame away 14680 // 2: Not more than MAX_SILENT_FRAME_DURATION away 14681 // 3: currentTime (aka nextPtsNorm) is not 0 14682 // 4: remuxing with video (videoTimeOffset !== undefined) 14683 else if (delta >= maxAudioFramesDrift * inputSampleDuration && duration < MAX_SILENT_FRAME_DURATION && alignedWithVideo) { 14684 let missing = Math.round(delta / inputSampleDuration); 14685 // Adjust nextPts so that silent samples are aligned with media pts. This will prevent media samples from 14686 // later being shifted if nextPts is based on timeOffset and delta is not a multiple of inputSampleDuration. 14687 nextPts = pts - missing * inputSampleDuration; 14688 if (nextPts < 0) { 14689 missing--; 14690 nextPts += inputSampleDuration; 14691 } 14692 if (i === 0) { 14693 this.nextAudioPts = nextAudioPts = nextPts; 14694 } 14695 logger.warn(`[mp4-remuxer]: Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(1000 * delta / inputTimeScale)} ms gap.`); 14696 for (let j = 0; j < missing; j++) { 14697 const newStamp = Math.max(nextPts, 0); 14698 let fillFrame = AAC.getSilentFrame(track.manifestCodec || track.codec, track.channelCount); 14699 if (!fillFrame) { 14700 logger.log('[mp4-remuxer]: Unable to get silent frame for given audio codec; duplicating last frame instead.'); 14701 fillFrame = sample.unit.subarray(); 14702 } 14703 inputSamples.splice(i, 0, { 14704 unit: fillFrame, 14705 pts: newStamp 14706 }); 14707 nextPts += inputSampleDuration; 14708 i++; 14709 } 14710 } 14711 sample.pts = nextPts; 14712 nextPts += inputSampleDuration; 14713 } 14714 } 14715 let firstPTS = null; 14716 let lastPTS = null; 14717 let mdat; 14718 let mdatSize = 0; 14719 let sampleLength = inputSamples.length; 14720 while (sampleLength--) { 14721 mdatSize += inputSamples[sampleLength].unit.byteLength; 14722 } 14723 for (let j = 0, _nbSamples = inputSamples.length; j < _nbSamples; j++) { 14724 const audioSample = inputSamples[j]; 14725 const unit = audioSample.unit; 14726 let pts = audioSample.pts; 14727 if (lastPTS !== null) { 14728 // If we have more than one sample, set the duration of the sample to the "real" duration; the PTS diff with 14729 // the previous sample 14730 const prevSample = outputSamples[j - 1]; 14731 prevSample.duration = Math.round((pts - lastPTS) / scaleFactor); 14732 } else { 14733 if (contiguous && track.segmentCodec === 'aac') { 14734 // set PTS/DTS to expected PTS/DTS 14735 pts = nextAudioPts; 14736 } 14737 // remember first PTS of our audioSamples 14738 firstPTS = pts; 14739 if (mdatSize > 0) { 14740 /* concatenate the audio data and construct the mdat in place 14741 (need 8 more bytes to fill length and mdat type) */ 14742 mdatSize += offset; 14743 try { 14744 mdat = new Uint8Array(mdatSize); 14745 } catch (err) { 14746 this.observer.emit(Events.ERROR, Events.ERROR, { 14747 type: ErrorTypes.MUX_ERROR,
14748 details: ErrorDetails.REMUX_ALLOC_ERROR, 14749 fatal: false, 14750 error: err, 14751 bytes: mdatSize, 14752 reason: `fail allocating audio mdat ${mdatSize}` 14753 }); 14754 return; 14755 } 14756 if (!rawMPEG) { 14757 const view = new DataView(mdat.buffer); 14758 view.setUint32(0, mdatSize); 14759 mdat.set(MP4.types.mdat, 4); 14760 } 14761 } else { 14762 // no audio samples 14763 return; 14764 } 14765 } 14766 mdat.set(unit, offset); 14767 const unitLen = unit.byteLength; 14768 offset += unitLen; 14769 // Default the sample's duration to the computed mp4SampleDuration, which will either be 1024 for AAC or 1152 for MPEG 14770 // In the case that we have 1 sample, this will be the duration. If we have more than one sample, the duration 14771 // becomes the PTS diff with the previous sample 14772 outputSamples.push(new Mp4Sample(true, mp4SampleDuration, unitLen, 0)); 14773 lastPTS = pts; 14774 } 14775 14776 // We could end up with no audio samples if all input samples were overlapping with the previously remuxed ones 14777 const nbSamples = outputSamples.length; 14778 if (!nbSamples) { 14779 return; 14780 } 14781 14782 // The next audio sample PTS should be equal to last sample PTS + duration 14783 const lastSample = outputSamples[outputSamples.length - 1]; 14784 this.nextAudioPts = nextAudioPts = lastPTS + scaleFactor * lastSample.duration; 14785 14786 // Set the track samples from inputSamples to outputSamples before remuxing 14787 const moof = rawMPEG ? new Uint8Array(0) : MP4.moof(track.sequenceNumber++, firstPTS / scaleFactor, _extends({}, track, { 14788 samples: outputSamples 14789 })); 14790 14791 // Clear the track samples. This also clears the samples array in the demuxer, since the reference is shared 14792 track.samples = []; 14793 const start = firstPTS / inputTimeScale; 14794 const end = nextAudioPts / inputTimeScale; 14795 const type = 'audio'; 14796 const audioData = { 14797 data1: moof, 14798 data2: mdat, 14799 startPTS: start, 14800 endPTS: end, 14801 startDTS: start, 14802 endDTS: end, 14803 type, 14804 hasAudio: true, 14805 hasVideo: false, 14806 nb: nbSamples 14807 }; 14808 this.isAudioContiguous = true; 14809 return audioData; 14810 } 14811 remuxEmptyAudio(track, timeOffset, contiguous, videoData) { 14812 const inputTimeScale = track.inputTimeScale; 14813 const mp4timeScale = track.samplerate ? track.samplerate : inputTimeScale; 14814 const scaleFactor = inputTimeScale / mp4timeScale; 14815 const nextAudioPts = this.nextAudioPts; 14816 // sync with video's timestamp 14817 const initDTS = this._initDTS; 14818 const init90kHz = initDTS.baseTime * 90000 / initDTS.timescale; 14819 const startDTS = (nextAudioPts !== null ? nextAudioPts : videoData.startDTS * inputTimeScale) + init90kHz; 14820 const endDTS = videoData.endDTS * inputTimeScale + init90kHz; 14821 // one sample's duration value 14822 const frameDuration = scaleFactor * AAC_SAMPLES_PER_FRAME; 14823 // samples count of this segment's duration
14824 const nbSamples = Math.ceil((endDTS - startDTS) / frameDuration); 14825 // silent frame 14826 const silentFrame = AAC.getSilentFrame(track.manifestCodec || track.codec, track.channelCount); 14827 logger.warn('[mp4-remuxer]: remux empty Audio'); 14828 // Can't remux if we can't generate a silent frame... 14829 if (!silentFrame) { 14830 logger.trace('[mp4-remuxer]: Unable to remuxEmptyAudio since we were unable to get a silent frame for given audio codec'); 14831 return; 14832 } 14833 const samples = []; 14834 for (let i = 0; i < nbSamples; i++) { 14835 const stamp = startDTS + i * frameDuration; 14836 samples.push({ 14837 unit: silentFrame, 14838 pts: stamp, 14839 dts: stamp 14840 }); 14841 } 14842 track.samples = samples; 14843 return this.remuxAudio(track, timeOffset, contiguous, false); 14844 } 14845} 14846function normalizePts(value, reference) { 14847 let offset; 14848 if (reference === null) { 14849 return value; 14850 } 14851 if (reference < value) { 14852 // - 2^33 14853 offset = -8589934592; 14854 } else { 14855 // + 2^33 14856 offset = 8589934592; 14857 } 14858 /* PTS is 33bit (from 0 to 2^33 -1) 14859 if diff between value and reference is bigger than half of the amplitude (2^32) then it means that 14860 PTS looping occured. fill the gap */ 14861 while (Math.abs(value - reference) > 4294967296) { 14862 value += offset; 14863 } 14864 return value; 14865} 14866function findKeyframeIndex(samples) { 14867 for (let i = 0; i < samples.length; i++) { 14868 if (samples[i].key) { 14869 return i; 14870 } 14871 } 14872 return -1; 14873} 14874function flushTextTrackMetadataCueSamples(track, timeOffset, initPTS, initDTS) { 14875 const length = track.samples.length; 14876 if (!length) { 14877 return; 14878 } 14879 const inputTimeScale = track.inputTimeScale; 14880 for (let index = 0; index < length; index++) { 14881 const sample = track.samples[index]; 14882 // setting id3 pts, dts to relative time 14883 // using this._initPTS and this._initDTS to calculate relative time 14884 sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 14885 sample.dts = normalizePts(sample.dts - initDTS.baseTime * inputTimeScale / initDTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 14886 } 14887 const samples = track.samples; 14888 track.samples = []; 14889 return { 14890 samples 14891 }; 14892} 14893function flushTextTrackUserdataCueSamples(track, timeOffset, initPTS) { 14894 const length = track.samples.length; 14895 if (!length) { 14896 return; 14897 } 14898 const inputTimeScale = track.inputTimeScale; 14899 for (let index = 0; index < length; index++) { 14900 const sample = track.samples[index]; 14901 // setting text pts, dts to relative time 14902 // using this._initPTS and this._initDTS to calculate relative time 14903 sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 14904 } 14905 track.samples.sort((a, b) => a.pts - b.pts); 14906 const samples = track.samples; 14907 track.samples = []; 14908 return { 14909 samples 14910 }; 14911} 14912class Mp4Sample { 14913 constructor(isKeyframe, duration, size, cts) { 14914 this.size = void 0; 14915 this.duration = void 0; 14916 this.cts = void 0; 14917 this.flags = void 0; 14918 this.duration = duration; 14919 this.size = size; 14920 this.cts = cts; 14921 this.flags = { 14922 isLeading: 0, 14923 isDependedOn: 0, 14924 hasRedundancy: 0, 14925 degradPrio: 0, 14926 dependsOn: isKeyframe ? 2 : 1, 14927 isNonSync: isKeyframe ? 0 : 1 14928 }; 14929 } 14930} 14931 14932class PassThroughRemuxer { 14933 constructor() { 14934 this.emitInitSegment = false; 14935 this.audioCodec = void 0; 14936 this.videoCodec = void 0; 14937 this.initData = void 0; 14938 this.initPTS = null; 14939 this.initTracks = void 0; 14940 this.lastEndTime = null; 14941 } 14942 destroy() {} 14943 resetTimeStamp(defaultInitPTS) { 14944 this.initPTS = defaultInitPTS; 14945 this.lastEndTime = null; 14946 } 14947 resetNextTimestamp() { 14948 this.lastEndTime = null; 14949 } 14950 resetInitSegment(initSegment, audioCodec, videoCodec, decryptdata) { 14951 this.audioCodec = audioCodec; 14952 this.videoCodec = videoCodec; 14953 this.generateInitSegment(patchEncyptionData(initSegment, decryptdata)); 14954 this.emitInitSegment = true; 14955 } 14956 generateInitSegment(initSegment) { 14957 let { 14958 audioCodec, 14959 videoCodec 14960 } = this; 14961 if (!(initSegment != null && initSegment.byteLength)) { 14962 this.initTracks = undefined; 14963 this.initData = undefined; 14964 return; 14965 } 14966 const initData = this.initData = parseInitSegment(initSegment); 14967 14968 // Get codec from initSegment or fallback to default 14969 if (initData.audio) { 14970 audioCodec = getParsedTrackCodec(initData.audio, ElementaryStreamTypes.AUDIO); 14971 } 14972 if (initData.video) { 14973 videoCodec = getParsedTrackCodec(initData.video, ElementaryStreamTypes.VIDEO); 14974 } 14975 const tracks = {}; 14976 if (initData.audio && initData.video) { 14977 tracks.audiovideo = { 14978 container: 'video/mp4', 14979 codec: audioCodec + ',' + videoCodec, 14980 initSegment, 14981 id: 'main' 14982 }; 14983 } else if (initData.audio) { 14984 tracks.audio = { 14985 container: 'audio/mp4', 14986 codec: audioCodec, 14987 initSegment, 14988 id: 'audio' 14989 }; 14990 } else if (initData.video) { 14991 tracks.video = { 14992 container: 'video/mp4', 14993 codec: videoCodec, 14994 initSegment, 14995 id: 'main' 14996 }; 14997 } else { 14998 logger.warn('[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes.'); 14999 } 15000 this.initTracks = tracks; 15001 } 15002 remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOff
15002set) { 15003 var _initData, _initData2; 15004 let { 15005 initPTS, 15006 lastEndTime 15007 } = this; 15008 const result = { 15009 audio: undefined, 15010 video: undefined, 15011 text: textTrack, 15012 id3: id3Track, 15013 initSegment: undefined 15014 }; 15015 15016 // If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the 15017 // lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update 15018 // the media duration (which is what timeOffset is provided as) before we need to process the next chunk. 15019 if (!isFiniteNumber(lastEndTime)) { 15020 lastEndTime = this.lastEndTime = timeOffset || 0; 15021 } 15022 15023 // The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only 15024 // audio or video (or both); adding it to video was an arbitrary choice. 15025 const data = videoTrack.samples; 15026 if (!(data != null && data.length)) { 15027 return result; 15028 } 15029 const initSegment = { 15030 initPTS: undefined, 15031 timescale: 1 15032 }; 15033 let initData = this.initData; 15034 if (!((_initData = initData) != null && _initData.length)) { 15035 this.generateInitSegment(data); 15036 initData = this.initData; 15037 } 15038 if (!((_initData2 = initData) != null && _initData2.length)) { 15039 // We can't remux if the initSegment could not be generated 15040 logger.warn('[passthrough-remuxer.ts]: Failed to generate initSegment.'); 15041 return result; 15042 } 15043 if (this.emitInitSegment) { 15044 initSegment.tracks = this.initTracks; 15045 this.emitInitSegment = false; 15046 } 15047 const duration = getDuration(data, initData); 15048 const startDTS = getStartDTS(initData, data); 15049 const decodeTime = startDTS === null ? timeOffset : startDTS; 15050 if (isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) || initSegment.timescale !== initPTS.timescale && accurateTimeOffset) { 15051 initSegment.initPTS = decodeTime - timeOffset; 15052 if (initPTS && initPTS.timescale === 1) { 15053 logger.warn(`Adjusting initPTS by ${initSegment.initPTS - initPTS.baseTime}`); 15054 } 15055 this.initPTS = initPTS = { 15056 baseTime: initSegment.initPTS, 15057 timescale: 1 15058 }; 15059 } 15060 const startTime = audioTrack ? decodeTime - initPTS.baseTime / initPTS.timescale : lastEndTime; 15061 const endTime = startTime + duration; 15062 offsetStartDTS(initData, data, initPTS.baseTime / initPTS.timescale); 15063 if (duration > 0) { 15064 this.lastEndTime = endTime; 15065 } else { 15066 logger.warn('Duration parsed from mp4 should be greater than zero'); 15067 this.resetNextTimestamp(); 15068 } 15069 const hasAudio = !!initData.audio; 15070 const hasVideo = !!initData.video; 15071 let type = ''; 15072 if (hasAudio) { 15073 type += 'audio'; 15074 } 15075 if (hasVideo) { 15076 type += 'video'; 15077 } 15078 const track = { 15079 data1: data, 15080 startPTS: startTime, 15081 startDTS: startTime, 15082 endPTS: endTime, 15083 endDTS: endTime, 15084 type, 15085 hasAudio, 15086 hasVideo, 15087 nb: 1, 15088 dropped: 0 15089 }; 15090 result.audio = track.type === 'audio' ? track : undefined; 15091 result.video = track.type !== 'audio' ? track : undefined; 15092 result.initSegment = initSegment; 15093 result.id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, initPTS, initPTS); 15094 if (textTrack.samples.length) { 15095 result.text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, initPTS); 15096 } 15097 return result; 15098 } 15099} 15100function isInvalidInitPts(initPTS, startDTS, timeOffset, duration) { 15101 if (initPTS === null) { 15102 return true; 15103 }
15104 // InitPTS is invalid when distance from program would be more than segment duration or a minimum of one second 15105 const minDuration = Math.max(duration, 1); 15106 const startTime = startDTS - initPTS.baseTime / initPTS.timescale; 15107 return Math.abs(startTime - timeOffset) > minDuration; 15108} 15109function getParsedTrackCodec(track, type) { 15110 const parsedCodec = track == null ? void 0 : track.codec; 15111 if (parsedCodec && parsedCodec.length > 4) { 15112 return parsedCodec; 15113 } 15114 if (type === ElementaryStreamTypes.AUDIO) { 15115 if (parsedCodec === 'ec-3' || parsedCodec === 'ac-3' || parsedCodec === 'alac') { 15116 return parsedCodec; 15117 } 15118 if (parsedCodec === 'fLaC' || parsedCodec === 'Opus') { 15119 // Opting not to get `preferManagedMediaSource` from player config for isSupported() check for simplicity 15120 const preferManagedMediaSource = false; 15121 return getCodecCompatibleName(parsedCodec, preferManagedMediaSource); 15122 } 15123 const result = 'mp4a.40.5'; 15124 logger.info(`Parsed audio codec "${parsedCodec}" or audio object type not handled. Using "${result}"`); 15125 return result; 15126 } 15127 // Provide defaults based on codec type 15128 // This allows for some playback of some fmp4 playlists without CODECS defined in manifest 15129 logger.warn(`Unhandled video codec "${parsedCodec}"`); 15130 if (parsedCodec === 'hvc1' || parsedCodec === 'hev1') { 15131 return 'hvc1.1.6.L120.90'; 15132 } 15133 if (parsedCodec === 'av01') { 15134 return 'av01.0.04M.08'; 15135 } 15136 return 'avc1.42e01e'; 15137} 15138 15139let now; 15140// performance.now() not available on WebWorker, at least on Safari Desktop 15141try { 15142 now = self.performance.now.bind(self.performance); 15143} catch (err) { 15144 logger.debug('Unable to use Performance API on this environment'); 15145 now = optionalSelf == null ? void 0 : optionalSelf.Date.now; 15146} 15147const muxConfig = [{ 15148 demux: MP4Demuxer, 15149 remux: PassThroughRemuxer 15150}, { 15151 demux: TSDemuxer, 15152 remux: MP4Remuxer 15153}, { 15154 demux: AACDemuxer, 15155 remux: MP4Remuxer 15156}, { 15157 demux: MP3Demuxer, 15158 remux: MP4Remuxer 15159}]; 15160{ 15161 muxConfig.splice(2, 0, { 15162 demux: AC3Demuxer, 15163 remux: MP4Remuxer 15164 }); 15165} 15166class Transmuxer { 15167 constructor(observer, typeSupported, config, vendor, id) { 15168 this.async = false; 15169 this.observer = void 0; 15170 this.typeSupported = void 0; 15171 this.config = void 0; 15172 this.vendor = void 0; 15173 this.id = void 0; 15174 this.demuxer = void 0; 15175 this.remuxer = void 0; 15176 this.decrypter = void 0; 15177 this.probe = void 0; 15178 this.decryptionPromise = null; 15179 this.transmuxConfig = void 0; 15180 this.currentTransmuxState = void 0; 15181 this.observer = observer; 15182 this.typeSupported = typeSupported; 15183 this.config = config; 15184 this.vendor = vendor; 15185 this.id = id; 15186 } 15187 configure(transmuxConfig) { 15188 this.transmuxConfig = transmuxConfig; 15189 if (this.decrypter) { 15190 this.decrypter.reset(); 15191 } 15192 } 15193 push(data, decryptdata, chunkMeta, state) { 15194 const stats = chunkMeta.transmuxing; 15195 stats.executeStart = now(); 15196 let uintData = new Uint8Array(data); 15197 const { 15198 currentTransmuxState, 15199 transmuxConfig 15200 } = this; 15201 if (state) { 15202 this.currentTransmuxState = state; 15203 } 15204 const { 15205 contiguous, 15206 discontinuity, 15207 trackSwitch, 15208 accurateTimeOffset, 15209 timeOffset, 15210 initSegmentChange 15211 } = state || currentTransmuxState; 15212 const { 15213 audioCodec, 15214 videoCodec, 15215 defaultInitPts, 15216 duration, 15217 initSegmentData 15218 } = transmuxConfig; 15219 const keyData = getEncryptionType(uintData, decryptdata); 15220 if (keyData && keyData.method === 'AES-128') { 15221 const decrypter = this.getDecrypter(); 15222 // Software decryption is synchronous; webCrypto is not 15223 if (decrypter.isSync()) { 15224 // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached 15225 // data is handled in the flush() call 15226 let decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer); 15227 // For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress 15228 const loadingParts = chunkMeta.part > -1; 15229 if (loadingParts) { 15230 decryptedData = decrypter.flush(); 15231 } 15232 if (!decryptedData) { 15233 stats.executeEnd = now(); 15234 return emptyResult(chunkMeta); 15235 } 15236 uintData = new Uint8Array(decryptedData); 15237 } else { 15238 this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer).then(decryptedData => { 15239 // Calling push here is important; if flush() is called while this is still resolving, this ensures that 15240 // the decrypted data has been transmuxed 15241 const result = this.push(decryptedData, null, chunkMeta); 15242 this.decryptionPromise = null; 15243 return result; 15244 }); 15245 return this.decryptionPromise; 15246 } 15247 } 15248 const resetMuxers = this.needsProbing(discontinuity, trackSwitch); 15249 if (resetMuxers) { 15250 const error = this.configureTransmuxer(uintData); 15251 if (error) { 15252 logger.warn(`[transmuxer] ${error.message}`); 15253 this.observer.emit(Events.ERROR, Events.ERROR, { 15254 type: ErrorTypes.MEDIA_ERROR, 15255 details: ErrorDetails.FRAG_PARSING_ERROR, 15256 fatal: false, 15257 error, 15258 reason: error.message 15259 }); 15260 stats.executeEnd = now(); 15261 return emptyResult(chunkMeta); 15262 } 15263 } 15264 if (discontinuity || trackSwitch || initSegmentChange || resetMuxers) { 15265 this.resetInitSegment(initSegmentData, audioCodec, videoCodec, duration, decryptdata); 15266 } 15267 if (discontinuity || initSegmentChange || resetMuxers) { 15268 this.resetInitialTimestamp(defaultInitPts); 15269 } 15270 if (!contiguous) { 15271 this.resetContiguity(); 15272 } 15273 const result = this.transmux(uintData, keyData, timeOffset, accurateTimeOff
15273set, chunkMeta); 15274 const currentState = this.currentTransmuxState; 15275 currentState.contiguous = true; 15276 currentState.discontinuity = false; 15277 currentState.trackSwitch = false; 15278 stats.executeEnd = now(); 15279 return result; 15280 } 15281 15282 // Due to data caching, flush calls can produce more than one TransmuxerResult (hence the Array type) 15283 flush(chunkMeta) { 15284 const stats = chunkMeta.transmuxing; 15285 stats.executeStart = now(); 15286 const { 15287 decrypter, 15288 currentTransmuxState, 15289 decryptionPromise 15290 } = this; 15291 if (decryptionPromise) { 15292 // Upon resolution, the decryption promise calls push() and returns its TransmuxerResult up the stack. Therefore 15293 // only flushing is required for async decryption 15294 return decryptionPromise.then(() => { 15295 return this.flush(chunkMeta); 15296 }); 15297 } 15298 const transmuxResults = []; 15299 const { 15300 timeOffset 15301 } = currentTransmuxState; 15302 if (decrypter) { 15303 // The decrypter may have data cached, which needs to be demuxed. In this case we'll have two TransmuxResults 15304 // This happens in the case that we receive only 1 push call for a segment (either for non-progressive downloads, 15305 // or for progressive downloads with small segments) 15306 const decryptedData = decrypter.flush(); 15307 if (decryptedData) { 15308 // Push always returns a TransmuxerResult if decryptdata is null 15309 transmuxResults.push(this.push(decryptedData, null, chunkMeta)); 15310 } 15311 } 15312 const { 15313 demuxer, 15314 remuxer 15315 } = this; 15316 if (!demuxer || !remuxer) { 15317 // If probing failed, then Hls.js has been given content its not able to handle 15318 stats.executeEnd = now(); 15319 return [emptyResult(chunkMeta)]; 15320 } 15321 const demuxResultOrPromise = demuxer.flush(timeOffset); 15322 if (isPromise(demuxResultOrPromise)) { 15323 // Decrypt final SAMPLE-AES samples 15324 return demuxResultOrPromise.then(demuxResult => { 15325 this.flushRemux(transmuxResults, demuxResult, chunkMeta); 15326 return transmuxResults; 15327 }); 15328 } 15329 this.flushRemux(transmuxResults, demuxResultOrPromise, chunkMeta); 15330 return transmuxResults; 15331 } 15332 flushRemux(transmuxResults, demuxResult, chunkMeta) { 15333 const { 15334 audioTrack, 15335 videoTrack, 15336 id3Track, 15337 textTrack 15338 } = demuxResult; 15339 const { 15340 accurateTimeOffset, 15341 timeOffset 15342 } = this.currentTransmuxState; 15343 logger.log(`[transmuxer.ts]: Flushed fragment ${chunkMeta.sn}${chunkMeta.part > -1 ? ' p: ' + chunkMeta.part : ''} of level ${chunkMeta.level}`); 15344 const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, true, this.id); 15345 transmuxResults.push({ 15346 remuxResult, 15347 chunkMeta 15348 }); 15349 chunkMeta.transmuxing.executeEnd = now(); 15350 } 15351 resetInitialTimestamp(defaultInitPts) { 15352 const { 15353 demuxer, 15354 remuxer 15355 } = this; 15356 if (!demuxer || !remuxer) { 15357 return; 15358 } 15359 demuxer.resetTimeStamp(defaultInitPts); 15360 remuxer.resetTimeStamp(defaultInitPts); 15361 } 15362 resetContiguity() { 15363 const { 15364 demuxer, 15365 remuxer 15366 } = this; 15367 if (!demuxer || !remuxer) { 15368 return; 15369 } 15370 demuxer.resetContiguity(); 15371 remuxer.resetNextTimestamp(); 15372 } 15373 resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration, decryptdata) { 15374 const { 15375 demuxer, 15376 remuxer 15377 } = this; 15378 if (!demuxer || !remuxer) { 15379 return; 15380 } 15381 demuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration); 15382 remuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, decryptdata); 15383 } 15384 destroy() { 15385 if (this.demuxer) { 15386 this.demuxer.destroy(); 15387 this.demuxer = undefined; 15388 } 15389 if (this.remuxer) { 15390 this.remuxer.destroy(); 15391 this.remuxer = undefined; 15392 } 15393 } 15394 transmux(data, keyData, timeOffset, accurateTimeOffset, chunkMeta) { 15395 let result; 15396 if (keyData && keyData.method === 'SAMPLE-AES') { 15397 result = this.transmuxSampleAes(data, keyData, timeOffset, accurateTimeOffset, chunkMeta); 15398 } else { 15399 result = this.transmuxUnencrypted(data, timeOffset, accurateTimeOff
vendor: 3,877 bytes, lines 15399-15516
15399set, chunkMeta); 15400 } 15401 return result; 15402 } 15403 transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta) { 15404 const { 15405 audioTrack, 15406 videoTrack, 15407 id3Track, 15408 textTrack 15409 } = this.demuxer.demux(data, timeOffset, false, !this.config.progressive); 15410 const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, false, this.id); 15411 return { 15412 remuxResult, 15413 chunkMeta 15414 }; 15415 } 15416 transmuxSampleAes(data, decryptData, timeOffset, accurateTimeOffset, chunkMeta) { 15417 return this.demuxer.demuxSampleAes(data, decryptData, timeOffset).then(demuxResult => { 15418 const remuxResult = this.remuxer.remux(demuxResult.audioTrack, demuxResult.videoTrack, demuxResult.id3Track, demuxResult.textTrack, timeOffset, accurateTimeOffset, false, this.id); 15419 return { 15420 remuxResult, 15421 chunkMeta 15422 }; 15423 }); 15424 } 15425 configureTransmuxer(data) { 15426 const { 15427 config, 15428 observer, 15429 typeSupported, 15430 vendor 15431 } = this; 15432 // probe for content type 15433 let mux; 15434 for (let i = 0, len = muxConfig.length; i < len; i++) { 15435 var _muxConfig$i$demux; 15436 if ((_muxConfig$i$demux = muxConfig[i].demux) != null && _muxConfig$i$demux.probe(data)) { 15437 mux = muxConfig[i]; 15438 break; 15439 } 15440 } 15441 if (!mux) { 15442 return new Error('Failed to find demuxer by probing fragment data'); 15443 } 15444 // so let's check that current remuxer and demuxer are still valid 15445 const demuxer = this.demuxer; 15446 const remuxer = this.remuxer; 15447 const Remuxer = mux.remux; 15448 const Demuxer = mux.demux; 15449 if (!remuxer || !(remuxer instanceof Remuxer)) { 15450 this.remuxer = new Remuxer(observer, config, typeSupported, vendor); 15451 } 15452 if (!demuxer || !(demuxer instanceof Demuxer)) { 15453 this.demuxer = new Demuxer(observer, config, typeSupported); 15454 this.probe = Demuxer.probe; 15455 } 15456 } 15457 needsProbing(discontinuity, trackSwitch) { 15458 // in case of continuity change, or track switch 15459 // we might switch from content type (AAC container to TS container, or TS to fmp4 for example) 15460 return !this.demuxer || !this.remuxer || discontinuity || trackSwitch; 15461 } 15462 getDecrypter() { 15463 let decrypter = this.decrypter; 15464 if (!decrypter) { 15465 decrypter = this.decrypter = new Decrypter(this.config); 15466 } 15467 return decrypter; 15468 } 15469} 15470function getEncryptionType(data, decryptData) { 15471 let encryptionType = null; 15472 if (data.byteLength > 0 && (decryptData == null ? void 0 : decryptData.key) != null && decryptData.iv !== null && decryptData.method != null) { 15473 encryptionType = decryptData; 15474 } 15475 return encryptionType; 15476} 15477const emptyResult = chunkMeta => ({ 15478 remuxResult: {}, 15479 chunkMeta 15480}); 15481function isPromise(p) { 15482 return 'then' in p && p.then instanceof Function; 15483} 15484class TransmuxConfig { 15485 constructor(audioCodec, videoCodec, initSegmentData, duration, defaultInitPts) { 15486 this.audioCodec = void 0; 15487 this.videoCodec = void 0; 15488 this.initSegmentData = void 0; 15489 this.duration = void 0; 15490 this.defaultInitPts = void 0; 15491 this.audioCodec = audioCodec; 15492 this.videoCodec = videoCodec; 15493 this.initSegmentData = initSegmentData; 15494 this.duration = duration; 15495 this.defaultInitPts = defaultInitPts || null; 15496 } 15497} 15498class TransmuxState { 15499 constructor(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange) { 15500 this.discontinuity = void 0; 15501 this.contiguous = void 0; 15502 this.accurateTimeOffset = void 0; 15503 this.trackSwitch = void 0; 15504 this.timeOffset = void 0; 15505 this.initSegmentChange = void 0; 15506 this.discontinuity = discontinuity; 15507 this.contiguous = contiguous; 15508 this.accurateTimeOffset = accurateTimeOffset; 15509 this.trackSwitch = trackSwitch; 15510 this.timeOffset = timeOffset; 15511 this.initSegmentChange = initSegmentChange; 15512 } 15513} 15514 15515var eventemitter3 = {exports: {}}; 15516
vendor: 9,399 bytes, lines 15517-15852
15517(function (module) { 15518 15519 var has = Object.prototype.hasOwnProperty 15520 , prefix = '~'; 15521 15522 /** 15523 * Constructor to create a storage for our `EE` objects. 15524 * An `Events` instance is a plain object whose properties are event names. 15525 * 15526 * @constructor 15527 * @private 15528 */ 15529 function Events() {} 15530 15531 // 15532 // We try to not inherit from `Object.prototype`. In some engines creating an 15533 // instance in this way is faster than calling `Object.create(null)` directly. 15534 // If `Object.create(null)` is not supported we prefix the event names with a 15535 // character to make sure that the built-in object properties are not 15536 // overridden or used as an attack vector. 15537 // 15538 if (Object.create) { 15539 Events.prototype = Object.create(null); 15540 15541 // 15542 // This hack is needed because the `__proto__` property is still inherited in 15543 // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. 15544 // 15545 if (!new Events().__proto__) prefix = false; 15546 } 15547 15548 /** 15549 * Representation of a single event listener. 15550 * 15551 * @param {Function} fn The listener function. 15552 * @param {*} context The context to invoke the listener with. 15553 * @param {Boolean} [once=false] Specify if the listener is a one-time listener. 15554 * @constructor 15555 * @private 15556 */ 15557 function EE(fn, context, once) { 15558 this.fn = fn; 15559 this.context = context; 15560 this.once = once || false; 15561 } 15562 15563 /** 15564 * Add a listener for a given event. 15565 * 15566 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 15567 * @param {(String|Symbol)} event The event name. 15568 * @param {Function} fn The listener function. 15569 * @param {*} context The context to invoke the listener with. 15570 * @param {Boolean} once Specify if the listener is a one-time listener. 15571 * @returns {EventEmitter} 15572 * @private 15573 */ 15574 function addListener(emitter, event, fn, context, once) { 15575 if (typeof fn !== 'function') { 15576 throw new TypeError('The listener must be a function'); 15577 } 15578 15579 var listener = new EE(fn, context || emitter, once) 15580 , evt = prefix ? prefix + event : event; 15581 15582 if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 15583 else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 15584 else emitter._events[evt] = [emitter._events[evt], listener]; 15585 15586 return emitter; 15587 } 15588 15589 /** 15590 * Clear event by name. 15591 * 15592 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 15593 * @param {(String|Symbol)} evt The Event name. 15594 * @private 15595 */ 15596 function clearEvent(emitter, evt) { 15597 if (--emitter._eventsCount === 0) emitter._events = new Events(); 15598 else delete emitter._events[evt]; 15599 } 15600 15601 /** 15602 * Minimal `EventEmitter` interface that is molded against the Node.js 15603 * `EventEmitter` interface. 15604 * 15605 * @constructor 15606 * @public 15607 */ 15608 function EventEmitter() { 15609 this._events = new Events(); 15610 this._eventsCount = 0; 15611 } 15612 15613 /** 15614 * Return an array listing the events for which the emitter has registered 15615 * listeners. 15616 * 15617 * @returns {Array} 15618 * @public 15619 */ 15620 EventEmitter.prototype.eventNames = function eventNames() { 15621 var names = [] 15622 , events 15623 , name; 15624 15625 if (this._eventsCount === 0) return names; 15626 15627 for (name in (events = this._events)) { 15628 if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); 15629 } 15630 15631 if (Object.getOwnPropertySymbols) { 15632 return names.concat(Object.getOwnPropertySymbols(events)); 15633 } 15634 15635 return names; 15636 }; 15637 15638 /** 15639 * Return the listeners registered for a given event. 15640 * 15641 * @param {(String|Symbol)} event The event name. 15642 * @returns {Array} The registered listeners. 15643 * @public 15644 */ 15645 EventEmitter.prototype.listeners = function listeners(event) { 15646 var evt = prefix ? prefix + event : event 15647 , handlers = this._events[evt]; 15648 15649 if (!handlers) return []; 15650 if (handlers.fn) return [handlers.fn]; 15651 15652 for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { 15653 ee[i] = handlers[i].fn; 15654 } 15655 15656 return ee; 15657 }; 15658 15659 /** 15660 * Return the number of listeners listening to a given event. 15661 * 15662 * @param {(String|Symbol)} event The event name. 15663 * @returns {Number} The number of listeners. 15664 * @public 15665 */ 15666 EventEmitter.prototype.listenerCount = function listenerCount(event) { 15667 var evt = prefix ? prefix + event : event 15668 , listeners = this._events[evt]; 15669 15670 if (!listeners) return 0; 15671 if (listeners.fn) return 1; 15672 return listeners.length; 15673 }; 15674 15675 /** 15676 * Calls each of the listeners registered for a given event. 15677 * 15678 * @param {(String|Symbol)} event The event name. 15679 * @returns {Boolean} `true` if the event had listeners, else `false`. 15680 * @public 15681 */ 15682 EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 15683 var evt = prefix ? prefix + event : event; 15684 15685 if (!this._events[evt]) return false; 15686 15687 var listeners = this._events[evt] 15688 , len = arguments.length 15689 , args 15690 , i; 15691 15692 if (listeners.fn) { 15693 if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 15694 15695 switch (len) { 15696 case 1: return listeners.fn.call(listeners.context), true; 15697 case 2: return listeners.fn.call(listeners.context, a1), true; 15698 case 3: return listeners.fn.call(listeners.context, a1, a2), true; 15699 case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; 15700 case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; 15701 case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; 15702 } 15703 15704 for (i = 1, args = new Array(len -1); i < len; i++) { 15705 args[i - 1] = arguments[i]; 15706 } 15707 15708 listeners.fn.apply(listeners.context, args); 15709 } else { 15710 var length = listeners.length 15711 , j; 15712 15713 for (i = 0; i < length; i++) { 15714 if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 15715 15716 switch (len) { 15717 case 1: listeners[i].fn.call(listeners[i].context); break; 15718 case 2: listeners[i].fn.call(listeners[i].context, a1); break; 15719 case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 15720 case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 15721 default: 15722 if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 15723 args[j - 1] = arguments[j]; 15724 } 15725 15726 listeners[i].fn.apply(listeners[i].context, args); 15727 } 15728 } 15729 } 15730 15731 return true; 15732 }; 15733 15734 /** 15735 * Add a listener for a given event. 15736 * 15737 * @param {(String|Symbol)} event The event name. 15738 * @param {Function} fn The listener function. 15739 * @param {*} [context=this] The context to invoke the listener with. 15740 * @returns {EventEmitter} `this`. 15741 * @public 15742 */ 15743 EventEmitter.prototype.on = function on(event, fn, context) { 15744 return addListener(this, event, fn, context, false); 15745 }; 15746 15747 /** 15748 * Add a one-time listener for a given event. 15749 * 15750 * @param {(String|Symbol)} event The event name. 15751 * @param {Function} fn The listener function. 15752 * @param {*} [context=this] The context to invoke the listener with. 15753 * @returns {EventEmitter} `this`. 15754 * @public 15755 */ 15756 EventEmitter.prototype.once = function once(event, fn, context) { 15757 return addListener(this, event, fn, context, true); 15758 }; 15759 15760 /** 15761 * Remove the listeners of a given event. 15762 * 15763 * @param {(String|Symbol)} event The event name. 15764 * @param {Function} fn Only remove the listeners that match this function. 15765 * @param {*} context Only remove the listeners that have this context. 15766 * @param {Boolean} once Only remove one-time listeners. 15767 * @returns {EventEmitter} `this`. 15768 * @public 15769 */ 15770 EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { 15771 var evt = prefix ? prefix + event : event; 15772 15773 if (!this._events[evt]) return this; 15774 if (!fn) { 15775 clearEvent(this, evt); 15776 return this; 15777 } 15778 15779 var listeners = this._events[evt]; 15780 15781 if (listeners.fn) { 15782 if ( 15783 listeners.fn === fn && 15784 (!once || listeners.once) && 15785 (!context || listeners.context === context) 15786 ) { 15787 clearEvent(this, evt); 15788 } 15789 } else { 15790 for (var i = 0, events = [], length = listeners.length; i < length; i++) { 15791 if ( 15792 listeners[i].fn !== fn || 15793 (once && !listeners[i].once) || 15794 (context && listeners[i].context !== context) 15795 ) { 15796 events.push(listeners[i]); 15797 } 15798 } 15799 15800 // 15801 // Reset the array, or remove it completely if we have no more listeners. 15802 // 15803 if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; 15804 else clearEvent(this, evt); 15805 } 15806 15807 return this; 15808 }; 15809 15810 /** 15811 * Remove all listeners, or those of the specified event. 15812 * 15813 * @param {(String|Symbol)} [event] The event name. 15814 * @returns {EventEmitter} `this`. 15815 * @public 15816 */ 15817 EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 15818 var evt; 15819 15820 if (event) { 15821 evt = prefix ? prefix + event : event; 15822 if (this._events[evt]) clearEvent(this, evt); 15823 } else { 15824 this._events = new Events(); 15825 this._eventsCount = 0; 15826 } 15827 15828 return this; 15829 }; 15830 15831 // 15832 // Alias methods names because people roll like that. 15833 // 15834 EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 15835 EventEmitter.prototype.addListener = EventEmitter.prototype.on; 15836 15837 // 15838 // Expose the prefix. 15839 // 15840 EventEmitter.prefixed = prefix; 15841 15842 // 15843 // Allow `EventEmitter` to be imported as module namespace. 15844 // 15845 EventEmitter.EventEmitter = EventEmitter; 15846 15847 // 15848 // Expose the module. 15849 // 15850 { 15851 module.exports = EventEmitter; 15852 }
15852 15853} (eventemitter3)); 15854 15855var eventemitter3Exports = eventemitter3.exports; 15856var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports); 15857 15858class TransmuxerInterface {
15859 constructor(hls, id, onTransmuxComplete, onFlush) { 15860 this.error = null; 15861 this.hls = void 0; 15862 this.id = void 0; 15863 this.observer = void 0; 15864 this.frag = null; 15865 this.part = null; 15866 this.useWorker = void 0; 15867 this.workerContext = null; 15868 this.onwmsg = void 0; 15869 this.transmuxer = null; 15870 this.onTransmuxComplete = void 0; 15871 this.onFlush = void 0; 15872 const config = hls.config; 15873 this.hls = hls; 15874 this.id = id; 15875 this.useWorker = !!config.enableWorker; 15876 this.onTransmuxComplete = onTransmuxComplete; 15877 this.onFlush = onFlush; 15878 const forwardMessage = (ev, data) => { 15879 data = data || {}; 15880 data.frag = this.frag; 15881 data.id = this.id; 15882 if (ev === Events.ERROR) { 15883 this.error = data.error; 15884 } 15885 this.hls.trigger(ev, data); 15886 }; 15887 15888 // forward events to main thread 15889 this.observer = new EventEmitter(); 15890 this.observer.on(Events.FRAG_DECRYPTED, forwardMessage); 15891 this.observer.on(Events.ERROR, forwardMessage); 15892 const MediaSource = getMediaSource(config.preferManagedMediaSource) || { 15893 isTypeSupported: () => false 15894 }; 15895 const m2tsTypeSupported = { 15896 mpeg: MediaSource.isTypeSupported('audio/mpeg'), 15897 mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'), 15898 ac3: MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"') 15899 }; 15900 if (this.useWorker && typeof Worker !== 'undefined') { 15901 const canCreateWorker = config.workerPath || hasUMDWorker(); 15902 if (canCreateWorker) { 15903 try { 15904 if (config.workerPath) { 15905 logger.log(`loading Web Worker ${config.workerPath} for "${id}"`); 15906 this.workerContext = loadWorker(config.workerPath); 15907 } else { 15908 logger.log(`injecting Web Worker for "${id}"`); 15909 this.workerContext = injectWorker(); 15910 } 15911 this.onwmsg = event => this.onWorkerMessage(event); 15912 const { 15913 worker 15914 } = this.workerContext; 15915 worker.addEventListener('message', this.onwmsg); 15916 worker.onerror = event => { 15917 const error = new Error(`${event.message} (${event.filename}:${event.lineno})`); 15918 config.enableWorker = false;
15919 logger.warn(`Error in "${id}" Web Worker, fallback to inline`); 15920 this.hls.trigger(Events.ERROR, { 15921 type: ErrorTypes.OTHER_ERROR, 15922 details: ErrorDetails.INTERNAL_EXCEPTION, 15923 fatal: false, 15924 event: 'demuxerWorker', 15925 error 15926 }); 15927 }; 15928 worker.postMessage({ 15929 cmd: 'init', 15930 typeSupported: m2tsTypeSupported, 15931 vendor: '', 15932 id: id, 15933 config: JSON.stringify(config) 15934 }); 15935 } catch (err) { 15936 logger.warn(`Error setting up "${id}" Web Worker, fallback to inline`, err); 15937 this.resetWorker(); 15938 this.error = null; 15939 this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id); 15940 } 15941 return; 15942 } 15943 } 15944 this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id); 15945 } 15946 resetWorker() { 15947 if (this.workerContext) { 15948 const { 15949 worker, 15950 objectURL 15951 } = this.workerContext; 15952 if (objectURL) { 15953 // revoke the Object URL that was used to create transmuxer worker, so as not to leak it 15954 self.URL.revokeObjectURL(objectURL); 15955 } 15956 worker.removeEventListener('message', this.onwmsg); 15957 worker.onerror = null; 15958 worker.terminate(); 15959 this.workerContext = null; 15960 } 15961 } 15962 destroy() { 15963 if (this.workerContext) { 15964 this.resetWorker(); 15965 this.onwmsg = undefined; 15966 } else { 15967 const transmuxer = this.transmuxer; 15968 if (transmuxer) { 15969 transmuxer.destroy(); 15970 this.transmuxer = null; 15971 } 15972 } 15973 const observer = this.observer; 15974 if (observer) { 15975 observer.removeAllListeners(); 15976 } 15977 this.frag = null; 15978 // @ts-ignore 15979 this.observer = null; 15980 // @ts-ignore 15981 this.hls = null; 15982 } 15983 push(data, initSegmentData, audioCodec, videoCodec, frag, part, duration, accurateTimeOffset, chunkMeta, defaultInitPTS) { 15984 var _frag$initSegment, _lastFrag$initSegment; 15985 chunkMeta.transmuxing.start = self.performance.now(); 15986 const { 15987 transmuxer 15988 } = this; 15989 const timeOffset = part ? part.start : frag.start; 15990 // TODO: push "clear-lead" decrypt data for unencrypted fragments in streams with encrypted ones 15991 const decryptdata = frag.decryptdata; 15992 const lastFrag = this.frag; 15993 const discontinuity = !(lastFrag && frag.cc === lastFrag.cc); 15994 const trackSwitch = !(lastFrag && chunkMeta.level === lastFrag.level); 15995 const snDiff = lastFrag ? chunkMeta.sn - lastFrag.sn : -1; 15996 const partDiff = this.part ? chunkMeta.part - this.part.index : -1; 15997 const progressive = snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === (lastFrag == null ? void 0 : lastFrag.stats.chunkCount); 15998 const contiguous = !trackSwitch && (snDiff === 1 || snDiff === 0 && (partDiff === 1 || progressive && partDiff <= 0)); 15999 const now = self.performance.now(); 16000 if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { 16001 frag.stats.parsing.start = now; 16002 } 16003 if (part && (partDiff || !contiguous)) { 16004 part.stats.parsing.start = now; 16005 } 16006 const initSegmentChange = !(lastFrag && ((_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.url) === ((_lastFrag$initSegment = lastFrag.initSegment) == null ? void 0 : _lastFrag$initSegment.url)); 16007 const state = new TransmuxState(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange); 16008 if (!contiguous || discontinuity || initSegmentChange) { 16009 logger.log(`[transmuxer-interface, ${frag.type}]: Starting new transmux session for sn: ${chunkMeta.sn} p: ${chunkMeta.part} level: ${chunkMeta.level} id: ${chunkMeta.id} 16010 discontinuity: ${discontinuity} 16011 trackSwitch: ${trackSwitch} 16012 contiguous: ${contiguous} 16013 accurateTimeOffset: ${accurateTimeOffset} 16014 timeOffset: ${timeOffset} 16015 initSegmentChange: ${initSegmentChange}`); 16016 const config = new TransmuxConfig(audioCodec, videoCodec, initSegmentData, duration, defaultInitPTS); 16017 this.configureTransmuxer(config); 16018 } 16019 this.frag = frag; 16020 this.part = part; 16021 16022 // Frags with sn of 'initSegment' are not transmuxed 16023 if (this.workerContext) { 16024 // post fragment payload as transferable objects for ArrayBuffer (no copy) 16025 this.workerContext.worker.postMessage({ 16026 cmd: 'demux', 16027 data, 16028 decryptdata, 16029 chunkMeta, 16030 state 16031 }, data instanceof ArrayBuffer ? [data] : []); 16032 } else if (transmuxer) { 16033 const transmuxResult = transmuxer.push(data, decryptdata, chunkMeta, state);
16034 if (isPromise(transmuxResult)) { 16035 transmuxer.async = true; 16036 transmuxResult.then(data => { 16037 this.handleTransmuxComplete(data); 16038 }).catch(error => { 16039 this.transmuxerError(error, chunkMeta, 'transmuxer-interface push error'); 16040 }); 16041 } else { 16042 transmuxer.async = false; 16043 this.handleTransmuxComplete(transmuxResult); 16044 } 16045 } 16046 } 16047 flush(chunkMeta) { 16048 chunkMeta.transmuxing.start = self.performance.now(); 16049 const { 16050 transmuxer 16051 } = this; 16052 if (this.workerContext) { 16053 this.workerContext.worker.postMessage({ 16054 cmd: 'flush', 16055 chunkMeta 16056 }); 16057 } else if (transmuxer) { 16058 let transmuxResult = transmuxer.flush(chunkMeta); 16059 const asyncFlush = isPromise(transmuxResult); 16060 if (asyncFlush || transmuxer.async) { 16061 if (!isPromise(transmuxResult)) { 16062 transmuxResult = Promise.resolve(transmuxResult); 16063 } 16064 transmuxResult.then(data => { 16065 this.handleFlushResult(data, chunkMeta); 16066 }).catch(error => { 16067 this.transmuxerError(error, chunkMeta, 'transmuxer-interface flush error'); 16068 }); 16069 } else { 16070 this.handleFlushResult(transmuxResult, chunkMeta); 16071 } 16072 } 16073 } 16074 transmuxerError(error, chunkMeta, reason) { 16075 if (!this.hls) { 16076 return; 16077 } 16078 this.error = error; 16079 this.hls.trigger(Events.ERROR, { 16080 type: ErrorTypes.MEDIA_ERROR, 16081 details: ErrorDetails.FRAG_PARSING_ERROR, 16082 chunkMeta, 16083 frag: this.frag || undefined, 16084 fatal: false, 16085 error, 16086 err: error, 16087 reason 16088 }); 16089 } 16090 handleFlushResult(results, chunkMeta) { 16091 results.forEach(result => { 16092 this.handleTransmuxComplete(result); 16093 }); 16094 this.onFlush(chunkMeta); 16095 } 16096 onWorkerMessage(event) { 16097 const data = event.data; 16098 if (!(data != null && data.event)) { 16099 logger.warn(`worker message received with no ${data ? 'event name' : 'data'}`); 16100 return; 16101 } 16102 const hls = this.hls; 16103 if (!this.hls) { 16104 return; 16105 } 16106 switch (data.event) { 16107 case 'init': 16108 { 16109 var _this$workerContext; 16110 const objectURL = (_this$workerContext = this.workerContext) == null ? void 0 : _this$workerContext.objectURL; 16111 if (objectURL) { 16112 // revoke the Object URL that was used to create transmuxer worker, so as not to leak it 16113 self.URL.revokeObjectURL(objectURL); 16114 } 16115 break; 16116 } 16117 case 'transmuxComplete': 16118 { 16119 this.handleTransmuxComplete(data.data); 16120 break; 16121 } 16122 case 'flush': 16123 { 16124 this.onFlush(data.data); 16125 break; 16126 } 16127 16128 // pass logs from the worker thread to the main logger 16129 case 'workerLog': 16130 if (logger[data.data.logType]) { 16131 logger[data.data.logType](data.data.message); 16132 } 16133 break; 16134 default: 16135 { 16136 data.data = data.data || {}; 16137 data.data.frag = this.frag; 16138 data.data.id = this.id; 16139 hls.trigger(data.event, data.data); 16140 break; 16141 } 16142 } 16143 } 16144 configureTransmuxer(config) { 16145 const { 16146 transmuxer 16147 } = this; 16148 if (this.workerContext) { 16149 this.workerContext.worker.postMessage({ 16150 cmd: 'configure', 16151 config 16152 }); 16153 } else if (transmuxer) { 16154 transmuxer.configure(config); 16155 } 16156 } 16157 handleTransmuxComplete(result) { 16158 result.chunkMeta.transmuxing.end = self.performance.now(); 16159 this.onTransmuxComplete(result); 16160 } 16161} 16162 16163function subtitleOptionsIdentical(trackList1, trackList2) { 16164 if (trackList1.length !== trackList2.length) { 16165 return false; 16166 } 16167 for (let i = 0; i < trackList1.length; i++) { 16168 if (!mediaAttributesIdentical(trackList1[i].attrs, trackList2[i].attrs)) { 16169 return false; 16170 } 16171 } 16172 return true; 16173} 16174function mediaAttributesIdentical(attrs1, attrs2, customAttributes) { 16175 // Media options with the same rendition ID must be bit identical 16176 const stableRenditionId = attrs1['STABLE-RENDITION-ID']; 16177 if (stableRenditionId && !customAttributes) { 16178 return stableRenditionId === attrs2['STABLE-RENDITION-ID']; 16179 } 16180 // When rendition ID is not present, compare attributes 16181 return !(customAttributes || ['LANGUAGE', 'NAME', 'CHARACTERISTICS', 'AUTOSELECT', 'DEFAULT', 'FORCED', 'ASSOC-LANGUAGE']).some(subtitleAttribute =>
16181 attrs1[subtitleAttribute] !== attrs2[subtitleAttribute]); 16182} 16183function subtitleTrackMatchesTextTrack(subtitleTrack, textTrack) { 16184 return textTrack.label.toLowerCase() === subtitleTrack.name.toLowerCase() && (!textTrack.language || textTrack.language.toLowerCase() === (subtitleTrack.lang || '').toLowerCase()); 16185} 16186 16187const TICK_INTERVAL$2 = 100; // how often to tick in ms 16188 16189class AudioStreamController extends BaseStreamController { 16190 constructor(hls, fragmentTracker, keyLoader) { 16191 super(hls, fragmentTracker, keyLoader, '[audio-stream-controller]', PlaylistLevelType.AUDIO); 16192 this.videoBuffer = null; 16193 this.videoTrackCC = -1; 16194 this.waitingVideoCC = -1; 16195 this.bufferedTrack = null; 16196 this.switchingTrack = null; 16197 this.trackId = -1; 16198 this.waitingData = null; 16199 this.mainDetails = null; 16200 this.flushing = false; 16201 this.bufferFlushed = false; 16202 this.cachedTrackLoadedData = null; 16203 this._registerListeners(); 16204 } 16205 onHandlerDestroying() { 16206 this._unregisterListeners(); 16207 super.onHandlerDestroying(); 16208 this.mainDetails = null; 16209 this.bufferedTrack = null; 16210 this.switchingTrack = null; 16211 } 16212 _registerListeners() { 16213 const { 16214 hls 16215 } = this; 16216 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 16217 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 16218 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 16219 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 16220 hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); 16221 hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 16222 hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 16223 hls.on(Events.ERROR, this.onError, this); 16224 hls.on(Events.BUFFER_RESET, this.onBufferReset, this); 16225 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 16226 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 16227 hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 16228 hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 16229 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 16230 } 16231 _unregisterListeners() { 16232 const { 16233 hls 16234 } = this; 16235 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 16236 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 16237 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 16238 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 16239 hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); 16240 hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 16241 hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 16242 hls.off(Events.ERROR, this.onError, this); 16243 hls.off(Events.BUFFER_RESET, this.onBufferReset, this); 16244 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 16245 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 16246 hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 16247 hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 16248 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 16249 } 16250 16251 // INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value 16252 onInitPtsFound(event, { 16253 frag, 16254 id, 16255 initPTS, 16256 timescale 16257 }) { 16258 // Always update the new INIT PTS 16259 // Can change due level switch 16260 if (id === 'main') { 16261 const cc = frag.cc; 16262 this.initPTS[frag.cc] = { 16263 baseTime: initPTS, 16264 timescale 16265 }; 16266 this.log(`InitPTS for cc: ${cc} found from main: ${initPTS}`); 16267 this.videoTrackCC = cc; 16268 // If we are waiting, tick immediately to unblock audio fragment transmuxing 16269 if (this.state === State.WAITING_INIT_PTS) { 16270 this.tick(); 16271 } 16272 } 16273 } 16274 startLoad(startPosition) { 16275 if (!this.levels) { 16276 this.startPosition = startPosition; 16277 this.state = State.STOPPED; 16278 return; 16279 } 16280 const lastCurrentTime = this.lastCurrentTime; 16281 this.stopLoad(); 16282 this.setInterval(TICK_INTERVAL$2); 16283 if (lastCurrentTime > 0 && startPosition === -1) { 16284 this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); 16285 startPosition = lastCurrentTime; 16286 this.state = State.IDLE; 16287 } else { 16288 this.loadedmetadata = false;
16289 this.state = State.WAITING_TRACK; 16290 } 16291 this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition; 16292 this.tick(); 16293 } 16294 doTick() { 16295 switch (this.state) { 16296 case State.IDLE: 16297 this.doTickIdle(); 16298 break; 16299 case State.WAITING_TRACK: 16300 { 16301 var _levels$trackId; 16302 const { 16303 levels, 16304 trackId 16305 } = this; 16306 const details = levels == null ? void 0 : (_levels$trackId = levels[trackId]) == null ? void 0 : _levels$trackId.details; 16307 if (details) { 16308 if (this.waitForCdnTuneIn(details)) { 16309 break; 16310 } 16311 this.state = State.WAITING_INIT_PTS; 16312 } 16313 break; 16314 } 16315 case State.FRAG_LOADING_WAITING_RETRY: 16316 { 16317 var _this$media; 16318 const now = performance.now(); 16319 const retryDate = this.retryDate; 16320 // if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading 16321 if (!retryDate || now >= retryDate || (_this$media = this.media) != null && _this$media.seeking) { 16322 const { 16323 levels, 16324 trackId 16325 } = this; 16326 this.log('RetryDate reached, switch back to IDLE state'); 16327 this.resetStartWhenNotLoaded((levels == null ? void 0 : levels[trackId]) || null); 16328 this.state = State.IDLE; 16329 } 16330 break; 16331 } 16332 case State.WAITING_INIT_PTS: 16333 { 16334 // Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS 16335 const waitingData = this.waitingData; 16336 if (waitingData) { 16337 const { 16338 frag, 16339 part, 16340 cache, 16341 complete 16342 } = waitingData; 16343 if (this.initPTS[frag.cc] !== undefined) { 16344 this.waitingData = null; 16345 this.waitingVideoCC = -1; 16346 this.state = State.FRAG_LOADING; 16347 const payload = cache.flush(); 16348 const data = { 16349 frag, 16350 part, 16351 payload, 16352 networkDetails: null 16353 }; 16354 this._handleFragmentLoadProgress(data); 16355 if (complete) { 16356 super._handleFragmentLoadComplete(data); 16357 } 16358 } else if (this.videoTrackCC !== this.waitingVideoCC) { 16359 // Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found 16360 this.log(`Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${this.videoTrackCC}`); 16361 this.clearWaitingFragment(); 16362 } else { 16363 // Drop waiting fragment if an earlier fragment is needed 16364 const pos = this.getLoadPosition(); 16365 const bufferInfo = BufferHelper.bufferInfo(this.mediaBuffer, pos, this.config.maxBufferHole); 16366 const waitingFragmentAtPosition = fragmentWithinToleranceTest(bufferInfo.end, this.config.maxFragLookUpTolerance, frag); 16367 if (waitingFragmentAtPosition < 0) { 16368 this.log(`Waiting fragment cc (${frag.cc}) @ ${frag.start} cancelled because another fragment at ${bufferInfo.end} is needed`); 16369 this.clearWaitingFragment(); 16370 } 16371 } 16372 } else { 16373 this.state = State.IDLE; 16374 } 16375 } 16376 } 16377 this.onTickEnd(); 16378 } 16379 clearWaitingFragment() { 16380 const waitingData = this.waitingData; 16381 if (waitingData) { 16382 this.fragmentTracker.removeFragment(waitingData.frag); 16383 this.waitingData = null; 16384 this.waitingVideoCC = -1; 16385 this.state = State.IDLE; 16386 } 16387 } 16388 resetLoadingState() { 16389 this.clearWaitingFragment(); 16390 super.resetLoadingState(); 16391 } 16392 onTickEnd() { 16393 const { 16394 media 16395 } = this; 16396 if (!(media != null && media.readyState)) { 16397 // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0) 16398 return; 16399 } 16400 this.lastCurrentTime = media.currentTime; 16401 } 16402 doTickIdle() { 16403 const { 16404 hls, 16405 levels, 16406 media, 16407 trackId 16408 } = this; 16409 const config = hls.config; 16410 16411 // 1. if video not attached AND 16412 // start fragment already requested OR start frag prefetch not enabled 16413 // 2. if tracks or track not loaded and selected 16414 // then exit loop 16415 // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop 16416 if (!media && (this.startFragRequested || !config.startFragPrefetch) || !(levels != null && levels[trackId])) { 16417 return; 16418 } 16419 const levelInfo = levels[trackId];
16420 const trackDetails = levelInfo.details; 16421 if (!trackDetails || trackDetails.live && this.levelLastLoaded !== levelInfo || this.waitForCdnTuneIn(trackDetails)) { 16422 this.state = State.WAITING_TRACK; 16423 return; 16424 } 16425 const bufferable = this.mediaBuffer ? this.mediaBuffer : this.media; 16426 if (this.bufferFlushed && bufferable) { 16427 this.bufferFlushed = false; 16428 this.afterBufferFlushed(bufferable, ElementaryStreamTypes.AUDIO, PlaylistLevelType.AUDIO); 16429 } 16430 const bufferInfo = this.getFwdBufferInfo(bufferable, PlaylistLevelType.AUDIO); 16431 if (bufferInfo === null) { 16432 return; 16433 } 16434 const { 16435 bufferedTrack, 16436 switchingTrack 16437 } = this; 16438 if (!switchingTrack && this._streamEnded(bufferInfo, trackDetails)) { 16439 hls.trigger(Events.BUFFER_EOS, { 16440 type: 'audio' 16441 }); 16442 this.state = State.ENDED; 16443 return; 16444 } 16445 const mainBufferInfo = this.getFwdBufferInfo(this.videoBuffer ? this.videoBuffer : this.media, PlaylistLevelType.MAIN); 16446 const bufferLen = bufferInfo.len; 16447 const maxBufLen = this.getMaxBufferLength(mainBufferInfo == null ? void 0 : mainBufferInfo.len); 16448 const fragments = trackDetails.fragments; 16449 const start = fragments[0].start; 16450 let targetBufferTime = this.flushing ? this.getLoadPosition() : bufferInfo.end; 16451 if (switchingTrack && media) { 16452 const pos = this.getLoadPosition(); 16453 // STABLE 16454 if (bufferedTrack && !mediaAttributesIdentical(switchingTrack.attrs, bufferedTrack.attrs)) { 16455 targetBufferTime = pos; 16456 } 16457 // if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime 16458 if (trackDetails.PTSKnown && pos < start) { 16459 // if everything is buffered from pos to start or if audio buffer upfront, let's seek to start 16460 if (bufferInfo.end > start || bufferInfo.nextStart) { 16461 this.log('Alt audio track ahead of main track, seek to start of alt audio track'); 16462 media.currentTime = start + 0.05; 16463 } 16464 } 16465 } 16466 16467 // if buffer length is less than maxBufLen, or near the end, find a fragment to load 16468 if (bufferLen >= maxBufLen && !switchingTrack && targetBufferTime < fragments[fragments.length - 1].start) { 16469 return; 16470 } 16471 let frag = this.getNextFragment(targetBufferTime, trackDetails); 16472 let atGap = false; 16473 // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags 16474 if (frag && this.isLoopLoading(frag, targetBufferTime)) { 16475 atGap = !!frag.gap; 16476 frag = this.getNextFragmentLoopLoading(frag, trackDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); 16477 } 16478 if (!frag) { 16479 this.bufferFlushed = true; 16480 return; 16481 } 16482 16483 // Buffer audio up to one target duration ahead of main buffer 16484 const atBufferSyncLimit = mainBufferInfo && frag.start > mainBufferInfo.end + trackDetails.targetduration; 16485 if (atBufferSyncLimit || 16486 // Or wait for main buffer after buffing some audio 16487 !(mainBufferInfo != null && mainBufferInfo.len) && bufferInfo.len) { 16488 // Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo 16489 const mainFrag = this.getAppendedFrag(frag.start, PlaylistLevelType.MAIN); 16490 if (mainFrag === null) { 16491 return; 16492 } 16493 // Bridge gaps in main buffer 16494 atGap || (atGap = !!mainFrag.gap || !!atBufferSyncLimit && mainBufferInfo.len === 0); 16495 if (atBufferSyncLimit && !atGap || atGap && bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end) { 16496 return; 16497 } 16498 } 16499 this.loadFragment(frag, levelInfo, targetBufferTime); 16500 } 16501 getMaxBufferLength(mainBufferLength) { 16502 const maxConfigBuffer = super.getMaxBufferLength(); 16503 if (!mainBufferLength) { 16504 return maxConfigBuffer; 16505 } 16506 return Math.min(Math.max(maxConfigBuffer, mainBufferLength), this.config.maxMaxBufferLength); 16507 } 16508 onMediaDetaching() { 16509 this.videoBuffer = null; 16510 this.bufferFlushed = this.flushing = false; 16511 super.onMediaDetaching(); 16512 } 16513 onAudioTracksUpdated(event, { 16514 audioTracks 16515 }) { 16516 // Reset tranxmuxer is essential for large context switches (Content Steering) 16517 this.resetTransmuxer(); 16518 this.levels = audioTracks.map(mediaPlaylist => new Level(mediaPlaylist)); 16519 } 16520 onAudioTrackSwitching(event, data) { 16521 // if any URL found on new audio track, it is an alternate audio track 16522 const altAudio = !!data.url; 16523 this.trackId = data.id; 16524 const { 16525 fragCurrent 16526 } = this; 16527 if (fragCurrent) { 16528 fragCurrent.abortRequests(); 16529 this.removeUnbufferedFrags(fragCurrent.start); 16530 } 16531 this.resetLoadingState(); 16532 // destroy useless transmuxer when switching audio to main 16533 if (!altAudio) { 16534 this.resetTransmuxer(); 16535 } else { 16536 // switching to audio track, start timer if not already started 16537 this.setInterval(TICK_INTERVAL$2); 16538 } 16539 16540 // should we switch tracks ? 16541 if (altAudio) { 16542 this.switchingTrack = data; 16543 // main audio track are handled by stream-controller, just do something if switching to alt audio track 16544 this.state = State.IDLE; 16545 this.flushAudioIfNeeded(data); 16546 } else { 16547 this.switchingTrack = null; 16548 this.bufferedTrack = data; 16549 this.state = State.STOPPED; 16550 } 16551 this.tick(); 16552 } 16553 onManifestLoading() { 16554 this.fragmentTracker.removeAllFragments(); 16555 this.startPosition = this.lastCurrentTime = 0; 16556 this.bufferFlushed = this.flushing = false;
16557 this.levels = this.mainDetails = this.waitingData = this.bufferedTrack = this.cachedTrackLoadedData = this.switchingTrack = null; 16558 this.startFragRequested = false; 16559 this.trackId = this.videoTrackCC = this.waitingVideoCC = -1; 16560 } 16561 onLevelLoaded(event, data) { 16562 this.mainDetails = data.details; 16563 if (this.cachedTrackLoadedData !== null) { 16564 this.hls.trigger(Events.AUDIO_TRACK_LOADED, this.cachedTrackLoadedData); 16565 this.cachedTrackLoadedData = null; 16566 } 16567 } 16568 onAudioTrackLoaded(event, data) { 16569 var _track$details; 16570 if (this.mainDetails == null) { 16571 this.cachedTrackLoadedData = data; 16572 return; 16573 } 16574 const { 16575 levels 16576 } = this; 16577 const { 16578 details: newDetails, 16579 id: trackId 16580 } = data; 16581 if (!levels) { 16582 this.warn(`Audio tracks were reset while loading level ${trackId}`); 16583 return; 16584 } 16585 this.log(`Audio track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); 16586 const track = levels[trackId]; 16587 let sliding = 0; 16588 if (newDetails.live || (_track$details = track.details) != null && _track$details.live) { 16589 this.checkLiveUpdate(newDetails); 16590 const mainDetails = this.mainDetails; 16591 if (newDetails.deltaUpdateFailed || !mainDetails) { 16592 return; 16593 } 16594 if (!track.details && newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) { 16595 // Make sure our audio rendition is aligned with the "main" rendition, using 16596 // pdt as our reference times. 16597 alignMediaPlaylistByPDT(newDetails, mainDetails); 16598 sliding = newDetails.fragments[0].start; 16599 } else { 16600 var _this$levelLastLoaded; 16601 sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 16602 } 16603 } 16604 track.details = newDetails; 16605 this.levelLastLoaded = track; 16606 16607 // compute start position if we are aligned with the main playlist 16608 if (!this.startFragRequested && (this.mainDetails || !newDetails.live)) { 16609 this.setStartPosition(this.mainDetails || newDetails, sliding); 16610 } 16611 // only switch back to IDLE state if we were waiting for track to start downloading a new fragment 16612 if (this.state === State.WAITING_TRACK && !this.waitForCdnTuneIn(newDetails)) { 16613 this.state = State.IDLE; 16614 } 16615 16616 // trigger handler right now 16617 this.tick(); 16618 } 16619 _handleFragmentLoadProgress(data) { 16620 var _frag$initSegment; 16621 const { 16622 frag, 16623 part, 16624 payload 16625 } = data; 16626 const { 16627 config, 16628 trackId, 16629 levels 16630 } = this; 16631 if (!levels) { 16632 this.warn(`Audio tracks were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); 16633 return; 16634 } 16635 const track = levels[trackId]; 16636 if (!track) { 16637 this.warn('Audio track is undefined on fragment load progress'); 16638 return; 16639 } 16640 const details = track.details; 16641 if (!details) { 16642 this.warn('Audio track details undefined on fragment load progress'); 16643 this.removeUnbufferedFrags(frag.start); 16644 return; 16645 } 16646 const audioCodec = config.defaultAudioCodec || track.audioCodec || 'mp4a.40.2'; 16647 let transmuxer = this.transmuxer; 16648 if (!transmuxer) { 16649 transmuxer = this.transmuxer = new TransmuxerInterface(this.hls, PlaylistLevelType.AUDIO, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); 16650 } 16651 16652 // Check if we have video initPTS 16653 // If not we need to wait for it 16654 const initPTS = this.initPTS[frag.cc]; 16655 const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; 16656 if (initPTS !== undefined) { 16657 // this.log(`Transmuxing ${sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); 16658 // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) 16659 const accurateTimeOffset = false; // details.PTSKnown || !details.live; 16660 const partIndex = part ? part.index : -1; 16661 const partial = partIndex !== -1; 16662 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); 16663 transmuxer.push(payload, initSegmentData, audioCodec, '', frag, part, details.totalduration, accurateTimeOff
16663set, chunkMeta, initPTS); 16664 } else { 16665 this.log(`Unknown video PTS for cc ${frag.cc}, waiting for video PTS before demuxing audio frag ${frag.sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); 16666 const { 16667 cache 16668 } = this.waitingData = this.waitingData || { 16669 frag, 16670 part, 16671 cache: new ChunkCache(), 16672 complete: false 16673 }; 16674 cache.push(new Uint8Array(payload)); 16675 this.waitingVideoCC = this.videoTrackCC; 16676 this.state = State.WAITING_INIT_PTS; 16677 } 16678 } 16679 _handleFragmentLoadComplete(fragLoadedData) { 16680 if (this.waitingData) { 16681 this.waitingData.complete = true; 16682 return; 16683 } 16684 super._handleFragmentLoadComplete(fragLoadedData); 16685 } 16686 onBufferReset( /* event: Events.BUFFER_RESET */ 16687 ) { 16688 // reset reference to sourcebuffers 16689 this.mediaBuffer = this.videoBuffer = null; 16690 this.loadedmetadata = false; 16691 } 16692 onBufferCreated(event, data) { 16693 const audioTrack = data.tracks.audio; 16694 if (audioTrack) { 16695 this.mediaBuffer = audioTrack.buffer || null; 16696 } 16697 if (data.tracks.video) { 16698 this.videoBuffer = data.tracks.video.buffer || null; 16699 } 16700 } 16701 onFragBuffered(event, data) { 16702 const { 16703 frag, 16704 part 16705 } = data; 16706 if (frag.type !== PlaylistLevelType.AUDIO) { 16707 if (!this.loadedmetadata && frag.type === PlaylistLevelType.MAIN) { 16708 const bufferable = this.videoBuffer || this.media; 16709 if (bufferable) { 16710 const bufferedTimeRanges = BufferHelper.getBuffered(bufferable); 16711 if (bufferedTimeRanges.length) { 16712 this.loadedmetadata = true; 16713 } 16714 } 16715 } 16716 return; 16717 } 16718 if (this.fragContextChanged(frag)) { 16719 // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion 16720 // Avoid setting state back to IDLE or concluding the audio switch; otherwise, the switched-to track will not buffer 16721 this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.switchingTrack ? this.switchingTrack.name : 'false'}`); 16722 return; 16723 } 16724 if (frag.sn !== 'initSegment') { 16725 this.fragPrevious = frag; 16726 const track = this.switchingTrack; 16727 if (track) { 16728 this.bufferedTrack = track; 16729 this.switchingTrack = null; 16730 this.hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, track)); 16731 } 16732 } 16733 this.fragBufferedComplete(frag, part); 16734 } 16735 onError(event, data) { 16736 var _data$context; 16737 if (data.fatal) { 16738 this.state = State.ERROR; 16739 return; 16740 } 16741 switch (data.details) { 16742 case ErrorDetails.FRAG_GAP: 16743 case ErrorDetails.FRAG_PARSING_ERROR: 16744 case ErrorDetails.FRAG_DECRYPT_ERROR: 16745 case ErrorDetails.FRAG_LOAD_ERROR: 16746 case ErrorDetails.FRAG_LOAD_TIMEOUT: 16747 case ErrorDetails.KEY_LOAD_ERROR: 16748 case ErrorDetails.KEY_LOAD_TIMEOUT: 16749 this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data); 16750 break; 16751 case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: 16752 case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: 16753 case ErrorDetails.LEVEL_PARSING_ERROR: 16754 // in case of non fatal error while loading track, if not retrying to load track, switch back to IDLE 16755 if (!data.levelRetry && this.state === State.WAITING_TRACK && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.AUDIO_TRACK) { 16756 this.state = State.IDLE; 16757 } 16758 break; 16759 case ErrorDetails.BUFFER_APPEND_ERROR: 16760 case ErrorDetails.BUFFER_FULL_ERROR: 16761 if (!data.parent || data.parent !== 'audio') { 16762 return; 16763 } 16764 if (data.details === ErrorDetails.BUFFER_APPEND_ERROR) { 16765 this.resetLoadingState(); 16766 return; 16767 } 16768 if (this.reduceLengthAndFlushBuffer(data)) { 16769 this.bufferedTrack = null; 16770 super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); 16771 } 16772 break; 16773 case ErrorDetails.INTERNAL_EXCEPTION: 16774 this.recoverWorkerError(data); 16775 break; 16776 } 16777 } 16778 onBufferFlushing(event, { 16779 type 16780 }) { 16781 if (type !== ElementaryStreamTypes.VIDEO) { 16782 this.flushing = true; 16783 } 16784 } 16785 onBufferFlushed(event, { 16786 type 16787 }) { 16788 if (type !== ElementaryStreamTypes.VIDEO) { 16789 this.flushing = false;
vendor: 4,293 bytes, lines 16790-16930
16790 this.bufferFlushed = true; 16791 if (this.state === State.ENDED) { 16792 this.state = State.IDLE; 16793 } 16794 const mediaBuffer = this.mediaBuffer || this.media; 16795 if (mediaBuffer) { 16796 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.AUDIO); 16797 this.tick(); 16798 } 16799 } 16800 } 16801 _handleTransmuxComplete(transmuxResult) { 16802 var _id3$samples; 16803 const id = 'audio'; 16804 const { 16805 hls 16806 } = this; 16807 const { 16808 remuxResult, 16809 chunkMeta 16810 } = transmuxResult; 16811 const context = this.getCurrentContext(chunkMeta); 16812 if (!context) { 16813 this.resetWhenMissingContext(chunkMeta); 16814 return; 16815 } 16816 const { 16817 frag, 16818 part, 16819 level 16820 } = context; 16821 const { 16822 details 16823 } = level; 16824 const { 16825 audio, 16826 text, 16827 id3, 16828 initSegment 16829 } = remuxResult; 16830 16831 // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. 16832 // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. 16833 if (this.fragContextChanged(frag) || !details) { 16834 this.fragmentTracker.removeFragment(frag); 16835 return; 16836 } 16837 this.state = State.PARSING; 16838 if (this.switchingTrack && audio) { 16839 this.completeAudioSwitch(this.switchingTrack); 16840 } 16841 if (initSegment != null && initSegment.tracks) { 16842 const mapFragment = frag.initSegment || frag; 16843 this._bufferInitSegment(level, initSegment.tracks, mapFragment, chunkMeta); 16844 hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { 16845 frag: mapFragment, 16846 id, 16847 tracks: initSegment.tracks 16848 }); 16849 // Only flush audio from old audio tracks when PTS is known on new audio track 16850 } 16851 if (audio) { 16852 const { 16853 startPTS, 16854 endPTS, 16855 startDTS, 16856 endDTS 16857 } = audio; 16858 if (part) { 16859 part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { 16860 startPTS, 16861 endPTS, 16862 startDTS, 16863 endDTS 16864 }; 16865 } 16866 frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); 16867 this.bufferFragmentData(audio, frag, part, chunkMeta); 16868 } 16869 if (id3 != null && (_id3$samples = id3.samples) != null && _id3$samples.length) { 16870 const emittedID3 = _extends({ 16871 id, 16872 frag, 16873 details 16874 }, id3); 16875 hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); 16876 } 16877 if (text) { 16878 const emittedText = _extends({ 16879 id, 16880 frag, 16881 details 16882 }, text); 16883 hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); 16884 } 16885 } 16886 _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { 16887 if (this.state !== State.PARSING) { 16888 return; 16889 } 16890 // delete any video track found on audio transmuxer 16891 if (tracks.video) { 16892 delete tracks.video; 16893 } 16894 16895 // include levelCodec in audio and video tracks 16896 const track = tracks.audio; 16897 if (!track) { 16898 return; 16899 } 16900 track.id = 'audio'; 16901 const variantAudioCodecs = currentLevel.audioCodec; 16902 this.log(`Init audio buffer, container:${track.container}, codecs[level/parsed]=[${variantAudioCodecs}/${track.codec}]`); 16903 // SourceBuffer will use track.levelCodec if defined 16904 if (variantAudioCodecs && variantAudioCodecs.split(',').length === 1) { 16905 track.levelCodec = variantAudioCodecs; 16906 } 16907 this.hls.trigger(Events.BUFFER_CODECS, tracks); 16908 const initSegment = track.initSegment; 16909 if (initSegment != null && initSegment.byteLength) { 16910 const segment = { 16911 type: 'audio', 16912 frag, 16913 part: null, 16914 chunkMeta, 16915 parent: frag.type, 16916 data: initSegment 16917 }; 16918 this.hls.trigger(Events.BUFFER_APPENDING, segment); 16919 } 16920 // trigger handler right now 16921 this.tickImmediate(); 16922 } 16923 loadFragment(frag, track, targetBufferTime) { 16924 // only load if fragment is not loaded or if in audio switch 16925 const fragState = this.fragmentTracker.getState(frag); 16926 this.fragCurrent = frag; 16927 16928 // we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch 16929 if (this.switchingTrack || fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) { 16930 var _track$details2;
16931 if (frag.sn === 'initSegment') { 16932 this._loadInitSegment(frag, track); 16933 } else if ((_track$details2 = track.details) != null && _track$details2.live && !this.initPTS[frag.cc]) { 16934 this.log(`Waiting for video PTS in continuity counter ${frag.cc} of live stream before loading audio fragment ${frag.sn} of level ${this.trackId}`); 16935 this.state = State.WAITING_INIT_PTS; 16936 const mainDetails = this.mainDetails; 16937 if (mainDetails && mainDetails.fragments[0].start !== track.details.fragments[0].start) { 16938 alignMediaPlaylistByPDT(track.details, mainDetails); 16939 } 16940 } else { 16941 this.startFragRequested = true; 16942 super.loadFragment(frag, track, targetBufferTime); 16943 } 16944 } else { 16945 this.clearTrackerIfNeeded(frag); 16946 } 16947 } 16948 flushAudioIfNeeded(switchingTrack) { 16949 const { 16950 media, 16951 bufferedTrack 16952 } = this; 16953 const bufferedAttributes = bufferedTrack == null ? void 0 : bufferedTrack.attrs; 16954 const switchAttributes = switchingTrack.attrs; 16955 if (media && bufferedAttributes && (bufferedAttributes.CHANNELS !== switchAttributes.CHANNELS || bufferedTrack.name !== switchingTrack.name || bufferedTrack.lang !== switchingTrack.lang)) { 16956 this.log('Switching audio track : flushing all audio'); 16957 super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); 16958 this.bufferedTrack = null; 16959 } 16960 } 16961 completeAudioSwitch(switchingTrack) { 16962 const { 16963 hls 16964 } = this; 16965 this.flushAudioIfNeeded(switchingTrack); 16966 this.bufferedTrack = switchingTrack; 16967 this.switchingTrack = null; 16968 hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, switchingTrack)); 16969 } 16970} 16971 16972class AudioTrackController extends BasePlaylistController { 16973 constructor(hls) { 16974 super(hls, '[audio-track-controller]'); 16975 this.tracks = []; 16976 this.groupIds = null; 16977 this.tracksInGroup = []; 16978 this.trackId = -1; 16979 this.currentTrack = null; 16980 this.selectDefaultTrack = true; 16981 this.registerListeners(); 16982 } 16983 registerListeners() { 16984 const { 16985 hls 16986 } = this; 16987 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 16988 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 16989 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 16990 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 16991 hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 16992 hls.on(Events.ERROR, this.onError, this); 16993 } 16994 unregisterListeners() { 16995 const { 16996 hls 16997 } = this; 16998 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 16999 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 17000 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 17001 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 17002 hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 17003 hls.off(Events.ERROR, this.onError, this); 17004 } 17005 destroy() { 17006 this.unregisterListeners(); 17007 this.tracks.length = 0; 17008 this.tracksInGroup.length = 0; 17009 this.currentTrack = null; 17010 super.destroy(); 17011 } 17012 onManifestLoading() { 17013 this.tracks = []; 17014 this.tracksInGroup = []; 17015 this.groupIds = null; 17016 this.currentTrack = null; 17017 this.trackId = -1; 17018 this.selectDefaultTrack = true; 17019 } 17020 onManifestParsed(event, data) { 17021 this.tracks = data.audioTracks || []; 17022 } 17023 onAudioTrackLoaded(event, data) { 17024 const { 17025 id, 17026 groupId, 17027 details 17028 } = data; 17029 const trackInActiveGroup = this.tracksInGroup[id]; 17030 if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { 17031 this.warn(`Audio track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); 17032 return; 17033 } 17034 const curDetails = trackInActiveGroup.details; 17035 trackInActiveGroup.details = data.details; 17036 this.log(`Audio track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); 17037 if (id === this.trackId) { 17038 this.playlistLoaded(id, data, curDetails); 17039 } 17040 } 17041 onLevelLoading(event, data) { 17042 this.switchLevel(data.level); 17043 } 17044 onLevelSwitching(event, data) { 17045 this.switchLevel(data.level); 17046 } 17047 switchLevel(levelIndex) { 17048 const levelInfo = this.hls.levels[levelIndex]; 17049 if (!levelInfo) { 17050 return; 17051 } 17052 const audioGroups = levelInfo.audioGroups || null; 17053 const currentGroups = this.groupIds;
vendor: 4,728 bytes, lines 17054-17165
17054 let currentTrack = this.currentTrack; 17055 if (!audioGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (audioGroups == null ? void 0 : audioGroups.length) || audioGroups != null && audioGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { 17056 this.groupIds = audioGroups; 17057 this.trackId = -1; 17058 this.currentTrack = null; 17059 const audioTracks = this.tracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 17060 if (audioTracks.length) { 17061 // Disable selectDefaultTrack if there are no default tracks 17062 if (this.selectDefaultTrack && !audioTracks.some(track => track.default)) { 17063 this.selectDefaultTrack = false; 17064 } 17065 // track.id should match hls.audioTracks index 17066 audioTracks.forEach((track, i) => { 17067 track.id = i; 17068 }); 17069 } else if (!currentTrack && !this.tracksInGroup.length) { 17070 // Do not dispatch AUDIO_TRACKS_UPDATED when there were and are no tracks 17071 return; 17072 } 17073 this.tracksInGroup = audioTracks; 17074 17075 // Find preferred track 17076 const audioPreference = this.hls.config.audioPreference; 17077 if (!currentTrack && audioPreference) { 17078 const groupIndex = findMatchingOption(audioPreference, audioTracks, audioMatchPredicate); 17079 if (groupIndex > -1) { 17080 currentTrack = audioTracks[groupIndex]; 17081 } else { 17082 const allIndex = findMatchingOption(audioPreference, this.tracks); 17083 currentTrack = this.tracks[allIndex]; 17084 } 17085 } 17086 17087 // Select initial track 17088 let trackId = this.findTrackId(currentTrack); 17089 if (trackId === -1 && currentTrack) { 17090 trackId = this.findTrackId(null); 17091 } 17092 17093 // Dispatch events and load track if needed 17094 const audioTracksUpdated = { 17095 audioTracks 17096 }; 17097 this.log(`Updating audio tracks, ${audioTracks.length} track(s) found in group(s): ${audioGroups == null ? void 0 : audioGroups.join(',')}`); 17098 this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated); 17099 const selectedTrackId = this.trackId; 17100 if (trackId !== -1 && selectedTrackId === -1) { 17101 this.setAudioTrack(trackId); 17102 } else if (audioTracks.length && selectedTrackId === -1) { 17103 var _this$groupIds; 17104 const error = new Error(`No audio track selected for current audio group-ID(s): ${(_this$groupIds = this.groupIds) == null ? void 0 : _this$groupIds.join(',')} track count: ${audioTracks.length}`); 17105 this.warn(error.message); 17106 this.hls.trigger(Events.ERROR, { 17107 type: ErrorTypes.MEDIA_ERROR, 17108 details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR, 17109 fatal: true, 17110 error 17111 }); 17112 } 17113 } else if (this.shouldReloadPlaylist(currentTrack)) { 17114 // Retry playlist loading if no playlist is or has been loaded yet 17115 this.setAudioTrack(this.trackId); 17116 } 17117 } 17118 onError(event, data) { 17119 if (data.fatal || !data.context) { 17120 return; 17121 } 17122 if (data.context.type === PlaylistContextType.AUDIO_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { 17123 this.requestScheduled = -1; 17124 this.checkRetry(data); 17125 } 17126 } 17127 get allAudioTracks() { 17128 return this.tracks; 17129 } 17130 get audioTracks() { 17131 return this.tracksInGroup; 17132 } 17133 get audioTrack() { 17134 return this.trackId; 17135 } 17136 set audioTrack(newId) { 17137 // If audio track is selected from API then don't choose from the manifest default track 17138 this.selectDefaultTrack = false; 17139 this.setAudioTrack(newId); 17140 } 17141 setAudioOption(audioOption) { 17142 const hls = this.hls; 17143 hls.config.audioPreference = audioOption; 17144 if (audioOption) { 17145 const allAudioTracks = this.allAudioTracks; 17146 this.selectDefaultTrack = false; 17147 if (allAudioTracks.length) { 17148 // First see if current option matches (no switch op) 17149 const currentTrack = this.currentTrack; 17150 if (currentTrack && matchesOption(audioOption, currentTrack, audioMatchPredicate)) { 17151 return currentTrack; 17152 } 17153 // Find option in available tracks (tracksInGroup) 17154 const groupIndex = findMatchingOption(audioOption, this.tracksInGroup, audioMatchPredicate); 17155 if (groupIndex > -1) { 17156 const track = this.tracksInGroup[groupIndex]; 17157 this.setAudioTrack(groupIndex); 17158 return track; 17159 } else if (currentTrack) { 17160 // Find option in nearest level audio group 17161 let searchIndex = hls.loadLevel; 17162 if (searchIndex === -1) { 17163 searchIndex = hls.firstAutoLevel; 17164 } 17165 const switchIndex = findClosestLevelWithAudioGroup(audioOption, hl
vendor: 4,640 bytes, lines 17165-17307
17165s.levels, allAudioTracks, searchIndex, audioMatchPredicate); 17166 if (switchIndex === -1) { 17167 // could not find matching variant 17168 return null; 17169 } 17170 // and switch level to acheive the audio group switch 17171 hls.nextLoadLevel = switchIndex; 17172 } 17173 if (audioOption.channels || audioOption.audioCodec) { 17174 // Could not find a match with codec / channels predicate 17175 // Find a match without channels or codec 17176 const withoutCodecAndChannelsMatch = findMatchingOption(audioOption, allAudioTracks); 17177 if (withoutCodecAndChannelsMatch > -1) { 17178 return allAudioTracks[withoutCodecAndChannelsMatch]; 17179 } 17180 } 17181 } 17182 } 17183 return null; 17184 } 17185 setAudioTrack(newId) { 17186 const tracks = this.tracksInGroup; 17187 17188 // check if level idx is valid 17189 if (newId < 0 || newId >= tracks.length) { 17190 this.warn(`Invalid audio track id: ${newId}`); 17191 return; 17192 } 17193 17194 // stopping live reloading timer if any 17195 this.clearTimer(); 17196 this.selectDefaultTrack = false; 17197 const lastTrack = this.currentTrack; 17198 const track = tracks[newId]; 17199 const trackLoaded = track.details && !track.details.live; 17200 if (newId === this.trackId && track === lastTrack && trackLoaded) { 17201 return; 17202 } 17203 this.log(`Switching to audio-track ${newId} "${track.name}" lang:${track.lang} group:${track.groupId} channels:${track.channels}`); 17204 this.trackId = newId; 17205 this.currentTrack = track; 17206 this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, _objectSpread2({}, track)); 17207 // Do not reload track unless live 17208 if (trackLoaded) { 17209 return; 17210 } 17211 const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); 17212 this.loadPlaylist(hlsUrlParameters); 17213 } 17214 findTrackId(currentTrack) { 17215 const audioTracks = this.tracksInGroup; 17216 for (let i = 0; i < audioTracks.length; i++) { 17217 const track = audioTracks[i]; 17218 if (this.selectDefaultTrack && !track.default) { 17219 continue; 17220 } 17221 if (!currentTrack || matchesOption(currentTrack, track, audioMatchPredicate)) { 17222 return i; 17223 } 17224 } 17225 if (currentTrack) { 17226 const { 17227 name, 17228 lang, 17229 assocLang, 17230 characteristics, 17231 audioCodec, 17232 channels 17233 } = currentTrack; 17234 for (let i = 0; i < audioTracks.length; i++) { 17235 const track = audioTracks[i]; 17236 if (matchesOption({ 17237 name, 17238 lang, 17239 assocLang, 17240 characteristics, 17241 audioCodec, 17242 channels 17243 }, track, audioMatchPredicate)) { 17244 return i; 17245 } 17246 } 17247 for (let i = 0; i < audioTracks.length; i++) { 17248 const track = audioTracks[i]; 17249 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { 17250 return i; 17251 } 17252 } 17253 for (let i = 0; i < audioTracks.length; i++) { 17254 const track = audioTracks[i]; 17255 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { 17256 return i; 17257 } 17258 } 17259 } 17260 return -1; 17261 } 17262 loadPlaylist(hlsUrlParameters) { 17263 const audioTrack = this.currentTrack; 17264 if (this.shouldLoadPlaylist(audioTrack) && audioTrack) { 17265 super.loadPlaylist(); 17266 const id = audioTrack.id; 17267 const groupId = audioTrack.groupId; 17268 let url = audioTrack.url; 17269 if (hlsUrlParameters) { 17270 try { 17271 url = hlsUrlParameters.addDirectives(url); 17272 } catch (error) { 17273 this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); 17274 } 17275 } 17276 // track not retrieved yet, or live playlist we need to (re)load it 17277 this.log(`loading audio-track playlist ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}`); 17278 this.clearTimer(); 17279 this.hls.trigger(Events.AUDIO_TRACK_LOADING, { 17280 url, 17281 id, 17282 groupId, 17283 deliveryDirectives: hlsUrlParameters || null 17284 }); 17285 } 17286 } 17287} 17288 17289const TICK_INTERVAL$1 = 500; // how often to tick in ms 17290 17291class SubtitleStreamController extends BaseStreamController { 17292 constructor(hls, fragmentTracker, keyLoader) { 17293 super(hls, fragmentTracker, keyLoader, '[subtitle-stream-controller]', PlaylistLevelType.SUBTITLE); 17294 this.currentTrackId = -1; 17295 this.tracksBuffered = []; 17296 this.mainDetails = null; 17297 this._registerListeners(); 17298 } 17299 onHandlerDestroying() { 17300 this._unregisterListeners(); 17301 super.onHandlerDestroying(); 17302 this.mainDetails = null; 17303 } 17304 _registerListeners() { 17305 const { 17306 hls 17307 } = this;
vendor: 4,737 bytes, lines 17308-17444
17308 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 17309 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 17310 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 17311 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 17312 hls.on(Events.ERROR, this.onError, this); 17313 hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 17314 hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 17315 hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 17316 hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this); 17317 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 17318 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 17319 } 17320 _unregisterListeners() { 17321 const { 17322 hls 17323 } = this; 17324 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 17325 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 17326 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 17327 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 17328 hls.off(Events.ERROR, this.onError, this); 17329 hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 17330 hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 17331 hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 17332 hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this); 17333 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 17334 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 17335 } 17336 startLoad(startPosition) { 17337 this.stopLoad(); 17338 this.state = State.IDLE; 17339 this.setInterval(TICK_INTERVAL$1); 17340 this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition; 17341 this.tick(); 17342 } 17343 onManifestLoading() { 17344 this.mainDetails = null; 17345 this.fragmentTracker.removeAllFragments(); 17346 } 17347 onMediaDetaching() { 17348 this.tracksBuffered = []; 17349 super.onMediaDetaching(); 17350 } 17351 onLevelLoaded(event, data) { 17352 this.mainDetails = data.details; 17353 } 17354 onSubtitleFragProcessed(event, data) { 17355 const { 17356 frag, 17357 success 17358 } = data; 17359 this.fragPrevious = frag; 17360 this.state = State.IDLE; 17361 if (!success) { 17362 return; 17363 } 17364 const buffered = this.tracksBuffered[this.currentTrackId]; 17365 if (!buffered) { 17366 return; 17367 } 17368 17369 // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo 17370 // so we can re-use the logic used to detect how much has been buffered 17371 let timeRange; 17372 const fragStart = frag.start; 17373 for (let i = 0; i < buffered.length; i++) { 17374 if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) { 17375 timeRange = buffered[i]; 17376 break; 17377 } 17378 } 17379 const fragEnd = frag.start + frag.duration; 17380 if (timeRange) { 17381 timeRange.end = fragEnd; 17382 } else { 17383 timeRange = { 17384 start: fragStart, 17385 end: fragEnd 17386 }; 17387 buffered.push(timeRange); 17388 } 17389 this.fragmentTracker.fragBuffered(frag); 17390 this.fragBufferedComplete(frag, null); 17391 } 17392 onBufferFlushing(event, data) { 17393 const { 17394 startOffset, 17395 endOffset 17396 } = data; 17397 if (startOffset === 0 && endOffset !== Number.POSITIVE_INFINITY) { 17398 const endOffsetSubtitles = endOffset - 1; 17399 if (endOffsetSubtitles <= 0) { 17400 return; 17401 } 17402 data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles); 17403 this.tracksBuffered.forEach(buffered => { 17404 for (let i = 0; i < buffered.length;) { 17405 if (buffered[i].end <= endOffsetSubtitles) { 17406 buffered.shift(); 17407 continue; 17408 } else if (buffered[i].start < endOffsetSubtitles) { 17409 buffered[i].start = endOffsetSubtitles; 17410 } else { 17411 break; 17412 } 17413 i++; 17414 } 17415 }); 17416 this.fragmentTracker.removeFragmentsInRange(startOffset, endOffsetSubtitles, PlaylistLevelType.SUBTITLE); 17417 } 17418 } 17419 onFragBuffered(event, data) { 17420 if (!this.loadedmetadata && data.frag.type === PlaylistLevelType.MAIN) { 17421 var _this$media; 17422 if ((_this$media = this.media) != null && _this$media.buffered.length) { 17423 this.loadedmetadata = true; 17424 } 17425 } 17426 } 17427 17428 // If something goes wrong, proceed to next frag, if we were processing one. 17429 onError(event, data) { 17430 const frag = data.frag; 17431 if ((frag == null ? void 0 : frag.type) === PlaylistLevelType.SUBTITLE) { 17432 if (data.details === ErrorDetails.FRAG_GAP) { 17433 this.fragmentTracker.fragBuffered(frag, true); 17434 } 17435 if (this.fragCurrent) { 17436 this.fragCurrent.abortRequests(); 17437 } 17438 if (this.state !== State.STOPPED) { 17439 this.state = State.IDLE; 17440 } 17441 } 17442 } 17443 17444 // Got all new subtitle levels.
17445 onSubtitleTracksUpdated(event, { 17446 subtitleTracks 17447 }) { 17448 if (this.levels && subtitleOptionsIdentical(this.levels, subtitleTracks)) { 17449 this.levels = subtitleTracks.map(mediaPlaylist => new Level(mediaPlaylist)); 17450 return; 17451 } 17452 this.tracksBuffered = []; 17453 this.levels = subtitleTracks.map(mediaPlaylist => { 17454 const level = new Level(mediaPlaylist); 17455 this.tracksBuffered[level.id] = []; 17456 return level; 17457 }); 17458 this.fragmentTracker.removeFragmentsInRange(0, Number.POSITIVE_INFINITY, PlaylistLevelType.SUBTITLE); 17459 this.fragPrevious = null; 17460 this.mediaBuffer = null; 17461 } 17462 onSubtitleTrackSwitch(event, data) { 17463 var _this$levels; 17464 this.currentTrackId = data.id; 17465 if (!((_this$levels = this.levels) != null && _this$levels.length) || this.currentTrackId === -1) { 17466 this.clearInterval(); 17467 return; 17468 } 17469 17470 // Check if track has the necessary details to load fragments 17471 const currentTrack = this.levels[this.currentTrackId]; 17472 if (currentTrack != null && currentTrack.details) { 17473 this.mediaBuffer = this.mediaBufferTimeRanges; 17474 } else { 17475 this.mediaBuffer = null; 17476 } 17477 if (currentTrack) { 17478 this.setInterval(TICK_INTERVAL$1); 17479 } 17480 } 17481 17482 // Got a new set of subtitle fragments. 17483 onSubtitleTrackLoaded(event, data) { 17484 var _track$details; 17485 const { 17486 currentTrackId, 17487 levels 17488 } = this; 17489 const { 17490 details: newDetails, 17491 id: trackId 17492 } = data; 17493 if (!levels) { 17494 this.warn(`Subtitle tracks were reset while loading level ${trackId}`); 17495 return; 17496 } 17497 const track = levels[trackId]; 17498 if (trackId >= levels.length || !track) { 17499 return; 17500 } 17501 this.log(`Subtitle track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); 17502 this.mediaBuffer = this.mediaBufferTimeRanges; 17503 let sliding = 0; 17504 if (newDetails.live || (_track$details = track.details) != null && _track$details.live) { 17505 const mainDetails = this.mainDetails; 17506 if (newDetails.deltaUpdateFailed || !mainDetails) { 17507 return; 17508 } 17509 const mainSlidingStartFragment = mainDetails.fragments[0]; 17510 if (!track.details) { 17511 if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) { 17512 alignMediaPlaylistByPDT(newDetails, mainDetails); 17513 sliding = newDetails.fragments[0].start; 17514 } else if (mainSlidingStartFragment) { 17515 // line up live playlist with main so that fragments in range are loaded 17516 sliding = mainSlidingStartFragment.start; 17517 addSliding(newDetails, sliding); 17518 } 17519 } else { 17520 var _this$levelLastLoaded; 17521 sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 17522 if (sliding === 0 && mainSlidingStartFragment) { 17523 // realign with main when there is no overlap with last refresh 17524 sliding = mainSlidingStartFragment.start; 17525 addSliding(newDetails, sliding); 17526 } 17527 } 17528 } 17529 track.details = newDetails; 17530 this.levelLastLoaded = track; 17531 if (trackId !== currentTrackId) { 17532 return; 17533 } 17534 if (!this.startFragRequested && (this.mainDetails || !newDetails.live)) { 17535 this.setStartPosition(this.mainDetails || newDetails, sliding); 17536 } 17537 17538 // trigger handler right now 17539 this.tick(); 17540 17541 // If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload 17542 if (newDetails.live && !this.fragCurrent && this.media && this.state === State.IDLE) { 17543 const foundFrag = findFragmentByPTS(null, newDetails.fragments, this.media.currentTime, 0); 17544 if (!foundFrag) { 17545 this.warn('Subtitle playlist not aligned with playback'); 17546 track.details = undefined; 17547 } 17548 } 17549 } 17550 _handleFragmentLoadComplete(fragLoadedData) { 17551 const { 17552 frag, 17553 payload 17554 } = fragLoadedData; 17555 const decryptData = frag.decryptdata; 17556 const hls = this.hls; 17557 if (this.fragContextChanged(frag)) { 17558 return; 17559 } 17560 // check to see if the payload needs to be decrypted 17561 if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && decryptData.method === 'AES-128') { 17562 const startTime = performance.now(); 17563 // decrypt the subtitles
17564 this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer).catch(err => { 17565 hls.trigger(Events.ERROR, { 17566 type: ErrorTypes.MEDIA_ERROR, 17567 details: ErrorDetails.FRAG_DECRYPT_ERROR, 17568 fatal: false, 17569 error: err, 17570 reason: err.message, 17571 frag 17572 }); 17573 throw err; 17574 }).then(decryptedData => { 17575 const endTime = performance.now(); 17576 hls.trigger(Events.FRAG_DECRYPTED, { 17577 frag, 17578 payload: decryptedData, 17579 stats: { 17580 tstart: startTime, 17581 tdecrypt: endTime 17582 } 17583 }); 17584 }).catch(err => { 17585 this.warn(`${err.name}: ${err.message}`); 17586 this.state = State.IDLE; 17587 }); 17588 } 17589 } 17590 doTick() { 17591 if (!this.media) { 17592 this.state = State.IDLE; 17593 return; 17594 } 17595 if (this.state === State.IDLE) { 17596 const { 17597 currentTrackId, 17598 levels 17599 } = this; 17600 const track = levels == null ? void 0 : levels[currentTrackId]; 17601 if (!track || !levels.length || !track.details) { 17602 return; 17603 } 17604 const { 17605 config 17606 } = this; 17607 const currentTime = this.getLoadPosition(); 17608 const bufferedInfo = BufferHelper.bufferedInfo(this.tracksBuffered[this.currentTrackId] || [], currentTime, config.maxBufferHole); 17609 const { 17610 end: targetBufferTime, 17611 len: bufferLen 17612 } = bufferedInfo; 17613 const mainBufferInfo = this.getFwdBufferInfo(this.media, PlaylistLevelType.MAIN); 17614 const trackDetails = track.details; 17615 const maxBufLen = this.getMaxBufferLength(mainBufferInfo == null ? void 0 : mainBufferInfo.len) + trackDetails.levelTargetDuration; 17616 if (bufferLen > maxBufLen) { 17617 return; 17618 } 17619 const fragments = trackDetails.fragments; 17620 const fragLen = fragments.length; 17621 const end = trackDetails.edge; 17622 let foundFrag = null; 17623 const fragPrevious = this.fragPrevious; 17624 if (targetBufferTime < end) { 17625 const tolerance = config.maxFragLookUpTolerance; 17626 const lookupTolerance = targetBufferTime > end - tolerance ? 0 : tolerance; 17627 foundFrag = findFragmentByPTS(fragPrevious, fragments, Math.max(fragments[0].start, targetBufferTime), lookupTolerance); 17628 if (!foundFrag && fragPrevious && fragPrevious.start < fragments[0].start) { 17629 foundFrag = fragments[0]; 17630 } 17631 } else { 17632 foundFrag = fragments[fragLen - 1]; 17633 } 17634 if (!foundFrag) { 17635 return; 17636 } 17637 foundFrag = this.mapToInitFragWhenRequired(foundFrag); 17638 if (foundFrag.sn !== 'initSegment') { 17639 // Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment 17640 const curSNIdx = foundFrag.sn - trackDetails.startSN; 17641 const prevFrag = fragments[curSNIdx - 1]; 17642 if (prevFrag && prevFrag.cc === foundFrag.cc && this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED) { 17643 foundFrag = prevFrag; 17644 } 17645 } 17646 if (this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) { 17647 // only load if fragment is not loaded 17648 this.loadFragment(foundFrag, track, targetBufferTime); 17649 } 17650 } 17651 } 17652 getMaxBufferLength(mainBufferLength) { 17653 const maxConfigBuffer = super.getMaxBufferLength(); 17654 if (!mainBufferLength) { 17655 return maxConfigBuffer; 17656 } 17657 return Math.max(maxConfigBuffer, mainBufferLength); 17658 } 17659 loadFragment(frag, level, targetBufferTime) { 17660 this.fragCurrent = frag; 17661 if (frag.sn === 'initSegment') { 17662 this._loadInitSegment(frag, level); 17663 } else { 17664 this.startFragRequested = true; 17665 super.loadFragment(frag, level, targetBufferTime); 17666 } 17667 } 17668 get mediaBufferTimeRanges() { 17669 return new BufferableInstance(this.tracksBuffered[this.currentTrackId] || []); 17670 } 17671} 17672class BufferableInstance { 17673 constructor(timeranges) { 17674 this.buffered = void 0; 17675 const getRange = (name, index, length) => { 17676 index = index >>> 0; 17677 if (index > length - 1) { 17678 throw new DOMException(`Failed to execute '${name}' on 'TimeRanges': The index provided (${index}) is greater than the maximum bound (${length})`); 17679 } 17680 return timeranges[index][name]; 17681 }; 17682 this.buffered = { 17683 get length() { 17684 return timeranges.length; 17685 }, 17686 end(index) { 17687 return getRange('end', index, timeranges.length); 17688 }, 17689 start(index) { 17690 return getRange('start', index, timeranges.length); 17691 } 17692 }; 17693 } 17694} 17695 17696class SubtitleTrackController extends BasePlaylistController { 17697 constructor(hls) { 17698 super(hls, '[subtitle-track-controller]'); 17699 this.media = null; 17700 this.tracks = []; 17701 this.groupIds = null; 17702 this.tracksInGroup = []; 17703 this.trackId = -1; 17704 this.currentTrack = null; 17705 this.selectDefaultTrack = true; 17706 this.queuedDefaultTrack = -1; 17707 this.asyncPollTrackChange = () => this.pollTrackChange(0); 17708 this.useTextTrackPolling = false;
vendor: 5,523 bytes, lines 17709-17867
17709 this.subtitlePollingInterval = -1; 17710 this._subtitleDisplay = true; 17711 this.onTextTracksChanged = () => { 17712 if (!this.useTextTrackPolling) { 17713 self.clearInterval(this.subtitlePollingInterval); 17714 } 17715 // Media is undefined when switching streams via loadSource() 17716 if (!this.media || !this.hls.config.renderTextTracksNatively) { 17717 return; 17718 } 17719 let textTrack = null; 17720 const tracks = filterSubtitleTracks(this.media.textTracks); 17721 for (let i = 0; i < tracks.length; i++) { 17722 if (tracks[i].mode === 'hidden') { 17723 // Do not break in case there is a following track with showing. 17724 textTrack = tracks[i]; 17725 } else if (tracks[i].mode === 'showing') { 17726 textTrack = tracks[i]; 17727 break; 17728 } 17729 } 17730 17731 // Find internal track index for TextTrack 17732 const trackId = this.findTrackForTextTrack(textTrack); 17733 if (this.subtitleTrack !== trackId) { 17734 this.setSubtitleTrack(trackId); 17735 } 17736 }; 17737 this.registerListeners(); 17738 } 17739 destroy() { 17740 this.unregisterListeners(); 17741 this.tracks.length = 0; 17742 this.tracksInGroup.length = 0; 17743 this.currentTrack = null; 17744 this.onTextTracksChanged = this.asyncPollTrackChange = null; 17745 super.destroy(); 17746 } 17747 get subtitleDisplay() { 17748 return this._subtitleDisplay; 17749 } 17750 set subtitleDisplay(value) { 17751 this._subtitleDisplay = value; 17752 if (this.trackId > -1) { 17753 this.toggleTrackModes(); 17754 } 17755 } 17756 registerListeners() { 17757 const { 17758 hls 17759 } = this; 17760 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 17761 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 17762 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 17763 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 17764 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 17765 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 17766 hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 17767 hls.on(Events.ERROR, this.onError, this); 17768 } 17769 unregisterListeners() { 17770 const { 17771 hls 17772 } = this; 17773 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 17774 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 17775 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 17776 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 17777 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 17778 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 17779 hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 17780 hls.off(Events.ERROR, this.onError, this); 17781 } 17782 17783 // Listen for subtitle track change, then extract the current track ID. 17784 onMediaAttached(event, data) { 17785 this.media = data.media; 17786 if (!this.media) { 17787 return; 17788 } 17789 if (this.queuedDefaultTrack > -1) { 17790 this.subtitleTrack = this.queuedDefaultTrack; 17791 this.queuedDefaultTrack = -1; 17792 } 17793 this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks); 17794 if (this.useTextTrackPolling) { 17795 this.pollTrackChange(500); 17796 } else { 17797 this.media.textTracks.addEventListener('change', this.asyncPollTrackChange); 17798 } 17799 } 17800 pollTrackChange(timeout) { 17801 self.clearInterval(this.subtitlePollingInterval); 17802 this.subtitlePollingInterval = self.setInterval(this.onTextTracksChanged, timeout); 17803 } 17804 onMediaDetaching() { 17805 if (!this.media) { 17806 return; 17807 } 17808 self.clearInterval(this.subtitlePollingInterval); 17809 if (!this.useTextTrackPolling) { 17810 this.media.textTracks.removeEventListener('change', this.asyncPollTrackChange); 17811 } 17812 if (this.trackId > -1) { 17813 this.queuedDefaultTrack = this.trackId; 17814 } 17815 const textTracks = filterSubtitleTracks(this.media.textTracks); 17816 // Clear loaded cues on media detachment from tracks 17817 textTracks.forEach(track => { 17818 clearCurrentCues(track); 17819 }); 17820 // Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled. 17821 this.subtitleTrack = -1; 17822 this.media = null; 17823 } 17824 onManifestLoading() { 17825 this.tracks = []; 17826 this.groupIds = null; 17827 this.tracksInGroup = []; 17828 this.trackId = -1; 17829 this.currentTrack = null; 17830 this.selectDefaultTrack = true; 17831 } 17832 17833 // Fired whenever a new manifest is loaded. 17834 onManifestParsed(event, data) { 17835 this.tracks = data.subtitleTracks; 17836 } 17837 onSubtitleTrackLoaded(event, data) { 17838 const { 17839 id, 17840 groupId, 17841 details 17842 } = data; 17843 const trackInActiveGroup = this.tracksInGroup[id]; 17844 if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { 17845 this.warn(`Subtitle track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); 17846 return; 17847 } 17848 const curDetails = trackInActiveGroup.details; 17849 trackInActiveGroup.details = data.details; 17850 this.log(`Subtitle track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); 17851 if (id === this.trackId) { 17852 this.playlistLoaded(id, data, curDetails); 17853 } 17854 } 17855 onLevelLoading(event, data) { 17856 this.switchLevel(data.level); 17857 } 17858 onLevelSwitching(event, data) { 17859 this.switchLevel(data.level); 17860 } 17861 switchLevel(levelIndex) { 17862 const levelInfo = this.hls.levels[levelIndex]; 17863 if (!levelInfo) { 17864 return; 17865 } 17866 const subtitleGroups = levelInfo.subtitleGroups || null; 17867 const currentGroups = this.groupIds;
vendor: 4,378 bytes, lines 17868-17984
17868 let currentTrack = this.currentTrack; 17869 if (!subtitleGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (subtitleGroups == null ? void 0 : subtitleGroups.length) || subtitleGroups != null && subtitleGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { 17870 this.groupIds = subtitleGroups; 17871 this.trackId = -1; 17872 this.currentTrack = null; 17873 const subtitleTracks = this.tracks.filter(track => !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1); 17874 if (subtitleTracks.length) { 17875 // Disable selectDefaultTrack if there are no default tracks 17876 if (this.selectDefaultTrack && !subtitleTracks.some(track => track.default)) { 17877 this.selectDefaultTrack = false; 17878 } 17879 // track.id should match hls.audioTracks index 17880 subtitleTracks.forEach((track, i) => { 17881 track.id = i; 17882 }); 17883 } else if (!currentTrack && !this.tracksInGroup.length) { 17884 // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks 17885 return; 17886 } 17887 this.tracksInGroup = subtitleTracks; 17888 17889 // Find preferred track 17890 const subtitlePreference = this.hls.config.subtitlePreference; 17891 if (!currentTrack && subtitlePreference) { 17892 this.selectDefaultTrack = false; 17893 const groupIndex = findMatchingOption(subtitlePreference, subtitleTracks); 17894 if (groupIndex > -1) { 17895 currentTrack = subtitleTracks[groupIndex]; 17896 } else { 17897 const allIndex = findMatchingOption(subtitlePreference, this.tracks); 17898 currentTrack = this.tracks[allIndex]; 17899 } 17900 } 17901 17902 // Select initial track 17903 let trackId = this.findTrackId(currentTrack); 17904 if (trackId === -1 && currentTrack) { 17905 trackId = this.findTrackId(null); 17906 } 17907 17908 // Dispatch events and load track if needed 17909 const subtitleTracksUpdated = { 17910 subtitleTracks 17911 }; 17912 this.log(`Updating subtitle tracks, ${subtitleTracks.length} track(s) found in "${subtitleGroups == null ? void 0 : subtitleGroups.join(',')}" group-id`); 17913 this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated); 17914 if (trackId !== -1 && this.trackId === -1) { 17915 this.setSubtitleTrack(trackId); 17916 } 17917 } else if (this.shouldReloadPlaylist(currentTrack)) { 17918 // Retry playlist loading if no playlist is or has been loaded yet 17919 this.setSubtitleTrack(this.trackId); 17920 } 17921 } 17922 findTrackId(currentTrack) { 17923 const tracks = this.tracksInGroup; 17924 const selectDefault = this.selectDefaultTrack; 17925 for (let i = 0; i < tracks.length; i++) { 17926 const track = tracks[i]; 17927 if (selectDefault && !track.default || !selectDefault && !currentTrack) { 17928 continue; 17929 } 17930 if (!currentTrack || matchesOption(track, currentTrack)) { 17931 return i; 17932 } 17933 } 17934 if (currentTrack) { 17935 for (let i = 0; i < tracks.length; i++) { 17936 const track = tracks[i]; 17937 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { 17938 return i; 17939 } 17940 } 17941 for (let i = 0; i < tracks.length; i++) { 17942 const track = tracks[i]; 17943 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { 17944 return i; 17945 } 17946 } 17947 } 17948 return -1; 17949 } 17950 findTrackForTextTrack(textTrack) { 17951 if (textTrack) { 17952 const tracks = this.tracksInGroup; 17953 for (let i = 0; i < tracks.length; i++) { 17954 const track = tracks[i]; 17955 if (subtitleTrackMatchesTextTrack(track, textTrack)) { 17956 return i; 17957 } 17958 } 17959 } 17960 return -1; 17961 } 17962 onError(event, data) { 17963 if (data.fatal || !data.context) { 17964 return; 17965 } 17966 if (data.context.type === PlaylistContextType.SUBTITLE_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { 17967 this.checkRetry(data); 17968 } 17969 } 17970 get allSubtitleTracks() { 17971 return this.tracks; 17972 } 17973 17974 /** get alternate subtitle tracks list from playlist **/ 17975 get subtitleTracks() { 17976 return this.tracksInGroup; 17977 } 17978 17979 /** get/set index of the selected subtitle track (based on index in subtitle track lists) **/ 17980 get subtitleTrack() { 17981 return this.trackId; 17982 } 17983 set subtitleTrack(newId) { 17984 this.selectDefaultTrack = false;
17985 this.setSubtitleTrack(newId); 17986 } 17987 setSubtitleOption(subtitleOption) { 17988 this.hls.config.subtitlePreference = subtitleOption; 17989 if (subtitleOption) { 17990 const allSubtitleTracks = this.allSubtitleTracks; 17991 this.selectDefaultTrack = false; 17992 if (allSubtitleTracks.length) { 17993 // First see if current option matches (no switch op) 17994 const currentTrack = this.currentTrack; 17995 if (currentTrack && matchesOption(subtitleOption, currentTrack)) { 17996 return currentTrack; 17997 } 17998 // Find option in current group 17999 const groupIndex = findMatchingOption(subtitleOption, this.tracksInGroup); 18000 if (groupIndex > -1) { 18001 const track = this.tracksInGroup[groupIndex]; 18002 this.setSubtitleTrack(groupIndex); 18003 return track; 18004 } else if (currentTrack) { 18005 // If this is not the initial selection return null 18006 // option should have matched one in active group 18007 return null; 18008 } else { 18009 // Find the option in all tracks for initial selection 18010 const allIndex = findMatchingOption(subtitleOption, allSubtitleTracks); 18011 if (allIndex > -1) { 18012 return allSubtitleTracks[allIndex]; 18013 } 18014 } 18015 } 18016 } 18017 return null; 18018 } 18019 loadPlaylist(hlsUrlParameters) { 18020 super.loadPlaylist(); 18021 const currentTrack = this.currentTrack; 18022 if (this.shouldLoadPlaylist(currentTrack) && currentTrack) { 18023 const id = currentTrack.id; 18024 const groupId = currentTrack.groupId; 18025 let url = currentTrack.url; 18026 if (hlsUrlParameters) { 18027 try { 18028 url = hlsUrlParameters.addDirectives(url); 18029 } catch (error) { 18030 this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); 18031 } 18032 } 18033 this.log(`Loading subtitle playlist for id ${id}`); 18034 this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, { 18035 url, 18036 id, 18037 groupId, 18038 deliveryDirectives: hlsUrlParameters || null 18039 }); 18040 } 18041 } 18042 18043 /** 18044 * Disables the old subtitleTrack and sets current mode on the next subtitleTrack. 18045 * This operates on the DOM textTracks. 18046 * A value of -1 will disable all subtitle tracks. 18047 */ 18048 toggleTrackModes() { 18049 const { 18050 media 18051 } = this; 18052 if (!media) { 18053 return; 18054 } 18055 const textTracks = filterSubtitleTracks(media.textTracks); 18056 const currentTrack = this.currentTrack; 18057 let nextTrack; 18058 if (currentTrack) { 18059 nextTrack = textTracks.filter(textTrack => subtitleTrackMatchesTextTrack(currentTrack, textTrack))[0]; 18060 if (!nextTrack) { 18061 this.warn(`Unable to find subtitle TextTrack with name "${currentTrack.name}" and language "${currentTrack.lang}"`); 18062 } 18063 }
18064 [].slice.call(textTracks).forEach(track => { 18065 if (track.mode !== 'disabled' && track !== nextTrack) { 18066 track.mode = 'disabled'; 18067 } 18068 }); 18069 if (nextTrack) { 18070 const mode = this.subtitleDisplay ? 'showing' : 'hidden'; 18071 if (nextTrack.mode !== mode) { 18072 nextTrack.mode = mode; 18073 } 18074 } 18075 } 18076 18077 /** 18078 * This method is responsible for validating the subtitle index and periodically reloading if live. 18079 * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track. 18080 */ 18081 setSubtitleTrack(newId) { 18082 const tracks = this.tracksInGroup; 18083 18084 // setting this.subtitleTrack will trigger internal logic 18085 // if media has not been attached yet, it will fail 18086 // we keep a reference to the default track id 18087 // and we'll set subtitleTrack when onMediaAttached is triggered 18088 if (!this.media) { 18089 this.queuedDefaultTrack = newId; 18090 return; 18091 } 18092 18093 // exit if track id as already set or invalid 18094 if (newId < -1 || newId >= tracks.length || !isFiniteNumber(newId)) { 18095 this.warn(`Invalid subtitle track id: ${newId}`); 18096 return; 18097 } 18098 18099 // stopping live reloading timer if any 18100 this.clearTimer(); 18101 this.selectDefaultTrack = false; 18102 const lastTrack = this.currentTrack; 18103 const track = tracks[newId] || null; 18104 this.trackId = newId; 18105 this.currentTrack = track; 18106 this.toggleTrackModes(); 18107 if (!track) { 18108 // switch to -1 18109 this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { 18110 id: newId 18111 }); 18112 return; 18113 } 18114 const trackLoaded = !!track.details && !track.details.live; 18115 if (newId === this.trackId && track === lastTrack && trackLoaded) { 18116 return; 18117 } 18118 this.log(`Switching to subtitle-track ${newId}` + (track ? ` "${track.name}" lang:${track.lang} group:${track.groupId}` : '')); 18119 const { 18120 id, 18121 groupId = '', 18122 name, 18123 type, 18124 url 18125 } = track; 18126 this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { 18127 id, 18128 groupId, 18129 name, 18130 type, 18131 url 18132 }); 18133 const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); 18134 this.loadPlaylist(hlsUrlParameters); 18135 } 18136} 18137 18138class BufferOperationQueue { 18139 constructor(sourceBufferReference) { 18140 this.buffers = void 0; 18141 this.queues = { 18142 video: [], 18143 audio: [], 18144 audiovideo: [] 18145 }; 18146 this.buffers = sourceBufferReference; 18147 } 18148 append(operation, type, pending) { 18149 const queue = this.queues[type]; 18150 queue.push(operation); 18151 if (queue.length === 1 && !pending) { 18152 this.executeNext(type); 18153 } 18154 } 18155 insertAbort(operation, type) { 18156 const queue = this.queues[type]; 18157 queue.unshift(operation); 18158 this.executeNext(type); 18159 } 18160 appendBlocker(type) { 18161 let execute; 18162 const promise = new Promise(resolve => { 18163 execute = resolve; 18164 }); 18165 const operation = { 18166 execute, 18167 onStart: () => {}, 18168 onComplete: () => {}, 18169 onError: () => {} 18170 }; 18171 this.append(operation, type); 18172 return promise; 18173 } 18174 executeNext(type) { 18175 const queue = this.queues[type]; 18176 if (queue.length) { 18177 const operation = queue[0]; 18178 try { 18179 // Operations are expected to result in an 'updateend' event being fired. If not, the queue will lock. Operations 18180 // which do not end with this event must call _onSBUpdateEnd manually 18181 operation.execute(); 18182 } catch (error) { 18183 logger.warn(`[buffer-operation-queue]: Exception executing "${type}" SourceBuffer operation: ${error}`); 18184 operation.onError(error); 18185 18186 // Only shift the current operation off, otherwise the updateend handler will do this for us 18187 const sb = this.buffers[type]; 18188 if (!(sb != null && sb.updating)) { 18189 this.shiftAndExecuteNext(type); 18190 } 18191 } 18192 } 18193 } 18194 shiftAndExecuteNext(type) { 18195 this.queues[type].shift(); 18196 this.executeNext(type); 18197 } 18198 current(type) { 18199 return this.queues[type][0]; 18200 } 18201} 18202 18203const VIDEO_CODEC_PROFILE_REPLACE = /(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/; 18204class BufferController { 18205 constructor(hls) { 18206 // The level details used to determine duration, target-duration and live 18207 this.details = null; 18208 // cache the self generated object url to detect hijack of video tag 18209 this._objectUrl = null; 18210 // A queue of buffer operations which require the SourceBuffer to not be updating upon execution 18211 this.operationQueue = void 0; 18212 // References to event listeners for each SourceBuffer, so that they can be referenced for event removal 18213 this.listeners = void 0; 18214 this.hls = void 0; 18215 // The number of BUFFER_CODEC events received before any sourceBuffers are created 18216 this.bufferCodecEventsExpected = 0; 18217 // The total number of BUFFER_CODEC events received 18218 this._bufferCodecEventsTotal = 0; 18219 // A reference to the attached media element 18220 this.media = null; 18221 // A reference to the active media source 18222 this.mediaSource = null; 18223 // Last MP3 audio chunk appended 18224 this.lastMpegAudioChunk = null; 18225 this.appendSource = void 0; 18226 // counters 18227 this.appendErrors = { 18228 audio: 0, 18229 video: 0, 18230 audiovideo: 0 18231 }; 18232 this.tracks = {}; 18233 this.pendingTracks = {}; 18234 this.sourceBuffer = void 0; 18235 this.log = void 0; 18236 this.warn = void 0; 18237 this.error = void 0; 18238 this._onEndStreaming = event => { 18239 if (!this.hls) { 18240 return; 18241 }
18242 this.hls.pauseBuffering(); 18243 }; 18244 this._onStartStreaming = event => { 18245 if (!this.hls) { 18246 return; 18247 } 18248 this.hls.resumeBuffering(); 18249 }; 18250 // Keep as arrow functions so that we can directly reference these functions directly as event listeners 18251 this._onMediaSourceOpen = () => { 18252 const { 18253 media, 18254 mediaSource 18255 } = this; 18256 this.log('Media source opened'); 18257 if (media) { 18258 media.removeEventListener('emptied', this._onMediaEmptied); 18259 this.updateMediaElementDuration(); 18260 this.hls.trigger(Events.MEDIA_ATTACHED, { 18261 media, 18262 mediaSource: mediaSource 18263 }); 18264 } 18265 if (mediaSource) { 18266 // once received, don't listen anymore to sourceopen event 18267 mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); 18268 } 18269 this.checkPendingTracks(); 18270 }; 18271 this._onMediaSourceClose = () => { 18272 this.log('Media source closed'); 18273 }; 18274 this._onMediaSourceEnded = () => { 18275 this.log('Media source ended'); 18276 }; 18277 this._onMediaEmptied = () => { 18278 const { 18279 mediaSrc, 18280 _objectUrl 18281 } = this; 18282 if (mediaSrc !== _objectUrl) { 18283 logger.error(`Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`); 18284 } 18285 }; 18286 this.hls = hls; 18287 const logPrefix = '[buffer-controller]'; 18288 this.appendSource = isManagedMediaSource(getMediaSource(hls.config.preferManagedMediaSource)); 18289 this.log = logger.log.bind(logger, logPrefix); 18290 this.warn = logger.warn.bind(logger, logPrefix); 18291 this.error = logger.error.bind(logger, logPrefix); 18292 this._initSourceBuffer(); 18293 this.registerListeners(); 18294 } 18295 hasSourceTypes() { 18296 return this.getSourceBufferTypes().length > 0 || Object.keys(this.pendingTracks).length > 0; 18297 } 18298 destroy() { 18299 this.unregisterListeners(); 18300 this.details = null; 18301 this.lastMpegAudioChunk = null; 18302 // @ts-ignore 18303 this.hls = null; 18304 } 18305 registerListeners() { 18306 const { 18307 hls 18308 } = this; 18309 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 18310 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 18311 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18312 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18313 hls.on(Events.BUFFER_RESET, this.onBufferReset, this); 18314 hls.on(Events.BUFFER_APPENDING, this.onBufferAppending, this); 18315 hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); 18316 hls.on(Events.BUFFER_EOS, this.onBufferEos, this); 18317 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 18318 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 18319 hls.on(Events.FRAG_PARSED, this.onFragParsed, this); 18320 hls.on(Events.FRAG_CHANGED, this.onFragChanged, this); 18321 } 18322 unregisterListeners() { 18323 const { 18324 hls 18325 } = this; 18326 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 18327 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 18328 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18329 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18330 hls.off(Events.BUFFER_RESET, this.onBufferReset, this); 18331 hls.off(Events.BUFFER_APPENDING, this.onBufferAppending, this); 18332 hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 18333 hls.off(Events.BUFFER_EOS, this.onBufferEos, this); 18334 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 18335 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 18336 hls.off(Events.FRAG_PARSED, this.onFragParsed, this); 18337 hls.off(Events.FRAG_CHANGED, this.onFragChanged, this); 18338 } 18339 _initSourceBuffer() { 18340 this.sourceBuffer = {}; 18341 this.operationQueue = new BufferOperationQueue(this.sourceBuffer); 18342 this.listeners = { 18343 audio: [], 18344 video: [], 18345 audiovideo: [] 18346 }; 18347 this.appendErrors = { 18348 audio: 0, 18349 video: 0, 18350 audiovideo: 0 18351 }; 18352 this.lastMpegAudioChunk = null; 18353 } 18354 onManifestLoading() { 18355 this.bufferCodecEventsExpected = this._bufferCodecEventsTotal = 0; 18356 this.details = null; 18357 } 18358 onManifestParsed(event, data) { 18359 // in case of alt audio 2 BUFFER_CODECS events will be triggered, one per stream controller 18360 // sourcebuffers will be created all at once when the expected nb of tracks will be reached 18361 // in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller 18362 // it will contain the expected nb of source buffers, no need to compute it
18363 let codecEvents = 2; 18364 if (data.audio && !data.video || !data.altAudio || false) { 18365 codecEvents = 1; 18366 } 18367 this.bufferCodecEventsExpected = this._bufferCodecEventsTotal = codecEvents; 18368 this.log(`${this.bufferCodecEventsExpected} bufferCodec event(s) expected`); 18369 } 18370 onMediaAttaching(event, data) { 18371 const media = this.media = data.media; 18372 const MediaSource = getMediaSource(this.appendSource); 18373 if (media && MediaSource) { 18374 var _ms$constructor; 18375 const ms = this.mediaSource = new MediaSource(); 18376 this.log(`created media source: ${(_ms$constructor = ms.constructor) == null ? void 0 : _ms$constructor.name}`); 18377 // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound 18378 ms.addEventListener('sourceopen', this._onMediaSourceOpen); 18379 ms.addEventListener('sourceended', this._onMediaSourceEnded); 18380 ms.addEventListener('sourceclose', this._onMediaSourceClose); 18381 if (this.appendSource) { 18382 ms.addEventListener('startstreaming', this._onStartStreaming); 18383 ms.addEventListener('endstreaming', this._onEndStreaming); 18384 } 18385 18386 // cache the locally generated object url 18387 const objectUrl = this._objectUrl = self.URL.createObjectURL(ms); 18388 // link video and media Source 18389 if (this.appendSource) { 18390 try { 18391 media.removeAttribute('src'); 18392 // ManagedMediaSource will not open without disableRemotePlayback set to false or source alternatives 18393 const MMS = self.ManagedMediaSource; 18394 media.disableRemotePlayback = media.disableRemotePlayback || MMS && ms instanceof MMS; 18395 removeSourceChildren(media); 18396 addSource(media, objectUrl); 18397 media.load(); 18398 } catch (error) { 18399 media.src = objectUrl; 18400 } 18401 } else { 18402 media.src = objectUrl; 18403 } 18404 media.addEventListener('emptied', this._onMediaEmptied); 18405 } 18406 } 18407 onMediaDetaching() { 18408 const { 18409 media, 18410 mediaSource, 18411 _objectUrl 18412 } = this; 18413 if (mediaSource) { 18414 this.log('media source detaching'); 18415 if (mediaSource.readyState === 'open') { 18416 try { 18417 // endOfStream could trigger exception if any sourcebuffer is in updating state 18418 // we don't really care about checking sourcebuffer state here, 18419 // as we are anyway detaching the MediaSource 18420 // let's just avoid this exception to propagate 18421 mediaSource.endOfStream(); 18422 } catch (err) { 18423 this.warn(`onMediaDetaching: ${err.message} while calling endOfStream`); 18424 } 18425 } 18426 // Clean up the SourceBuffers by invoking onBufferReset 18427 this.onBufferReset(); 18428 mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); 18429 mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded); 18430 mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose); 18431 if (this.appendSource) { 18432 mediaSource.removeEventListener('startstreaming', this._onStartStreaming); 18433 mediaSource.removeEventListener('endstreaming', this._onEndStreaming); 18434 } 18435 18436 // Detach properly the MediaSource from the HTMLMediaElement as 18437 // suggested in https://github.com/w3c/media-source/issues/53. 18438 if (media) { 18439 media.removeEventListener('emptied', this._onMediaEmptied); 18440 if (_objectUrl) { 18441 self.URL.revokeObjectURL(_objectUrl); 18442 } 18443 18444 // clean up video tag src only if it's our own url. some external libraries might 18445 // hijack the video tag and change its 'src' without destroying the Hls instance first 18446 if (this.mediaSrc === _objectUrl) { 18447 media.removeAttribute('src'); 18448 if (this.appendSource) { 18449 removeSourceChildren(media); 18450 } 18451 media.load(); 18452 } else { 18453 this.warn('media|source.src was changed by a third party - skip cleanup'); 18454 } 18455 } 18456 this.mediaSource = null; 18457 this.media = null; 18458 this._objectUrl = null; 18459 this.bufferCodecEventsExpected = this._bufferCodecEventsTotal; 18460 this.pendingTracks = {}; 18461 this.tracks = {}; 18462 } 18463 this.hls.trigger(Events.MEDIA_DETACHED, undefined); 18464 } 18465 onBufferReset() {
18466 this.getSourceBufferTypes().forEach(type => { 18467 this.resetBuffer(type); 18468 }); 18469 this._initSourceBuffer(); 18470 } 18471 resetBuffer(type) { 18472 const sb = this.sourceBuffer[type]; 18473 try { 18474 if (sb) { 18475 var _this$mediaSource; 18476 this.removeBufferListeners(type); 18477 // Synchronously remove the SB from the map before the next call in order to prevent an async function from 18478 // accessing it 18479 this.sourceBuffer[type] = undefined; 18480 if ((_this$mediaSource = this.mediaSource) != null && _this$mediaSource.sourceBuffers.length) { 18481 this.mediaSource.removeSourceBuffer(sb); 18482 } 18483 } 18484 } catch (err) { 18485 this.warn(`onBufferReset ${type}`, err); 18486 } 18487 } 18488 onBufferCodecs(event, data) { 18489 const sourceBufferCount = this.getSourceBufferTypes().length; 18490 const trackNames = Object.keys(data); 18491 trackNames.forEach(trackName => { 18492 if (sourceBufferCount) { 18493 // check if SourceBuffer codec needs to change 18494 const track = this.tracks[trackName]; 18495 if (track && typeof track.buffer.changeType === 'function') { 18496 var _trackCodec; 18497 const { 18498 id, 18499 codec, 18500 levelCodec, 18501 container, 18502 metadata 18503 } = data[trackName]; 18504 const currentCodecFull = pickMostCompleteCodecName(track.codec, track.levelCodec); 18505 const currentCodec = currentCodecFull == null ? void 0 : currentCodecFull.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); 18506 let trackCodec = pickMostCompleteCodecName(codec, levelCodec); 18507 const nextCodec = (_trackCodec = trackCodec) == null ? void 0 : _trackCodec.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); 18508 if (trackCodec && currentCodec !== nextCodec) { 18509 if (trackName.slice(0, 5) === 'audio') { 18510 trackCodec = getCodecCompatibleName(trackCodec, this.appendSource); 18511 } 18512 const mimeType = `${container};codecs=${trackCodec}`; 18513 this.appendChangeType(trackName, mimeType); 18514 this.log(`switching codec ${currentCodecFull} to ${trackCodec}`); 18515 this.tracks[trackName] = { 18516 buffer: track.buffer, 18517 codec, 18518 container, 18519 levelCodec, 18520 metadata, 18521 id 18522 }; 18523 } 18524 } 18525 } else { 18526 // if source buffer(s) not created yet, appended buffer tracks in this.pendingTracks 18527 this.pendingTracks[trackName] = data[trackName]; 18528 } 18529 }); 18530 18531 // if sourcebuffers already created, do nothing ... 18532 if (sourceBufferCount) { 18533 return; 18534 } 18535 const bufferCodecEventsExpected = Math.max(this.bufferCodecEventsExpected - 1, 0); 18536 if (this.bufferCodecEventsExpected !== bufferCodecEventsExpected) { 18537 this.log(`${bufferCodecEventsExpected} bufferCodec event(s) expected ${trackNames.join(',')}`); 18538 this.bufferCodecEventsExpected = bufferCodecEventsExpected; 18539 } 18540 if (this.mediaSource && this.mediaSource.readyState === 'open') { 18541 this.checkPendingTracks(); 18542 } 18543 } 18544 appendChangeType(type, mimeType) { 18545 const { 18546 operationQueue 18547 } = this; 18548 const operation = { 18549 execute: () => { 18550 const sb = this.sourceBuffer[type]; 18551 if (sb) { 18552 this.log(`changing ${type} sourceBuffer type to ${mimeType}`); 18553 sb.changeType(mimeType); 18554 } 18555 operationQueue.shiftAndExecuteNext(type); 18556 }, 18557 onStart: () => {}, 18558 onComplete: () => {}, 18559 onError: error => { 18560 this.warn(`Failed to change ${type} SourceBuffer type`, error); 18561 } 18562 }; 18563 operationQueue.append(operation, type, !!this.pendingTracks[type]); 18564 } 18565 onBufferAppending(event, eventData) { 18566 const { 18567 hls, 18568 operationQueue, 18569 tracks 18570 } = this; 18571 const { 18572 data, 18573 type, 18574 frag, 18575 part, 18576 chunkMeta 18577 } = eventData; 18578 const chunkStats = chunkMeta.buffering[type]; 18579 const bufferAppendingStart = self.performance.now(); 18580 chunkStats.start = bufferAppendingStart; 18581 const fragBuffering = frag.stats.buffering;
18582 const partBuffering = part ? part.stats.buffering : null; 18583 if (fragBuffering.start === 0) { 18584 fragBuffering.start = bufferAppendingStart; 18585 } 18586 if (partBuffering && partBuffering.start === 0) { 18587 partBuffering.start = bufferAppendingStart; 18588 } 18589 18590 // TODO: Only update timestampOffset when audio/mpeg fragment or part is not contiguous with previously appended 18591 // Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended) 18592 // in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset` 18593 // is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos). 18594 // More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486 18595 const audioTrack = tracks.audio; 18596 let checkTimestampOffset = false; 18597 if (type === 'audio' && (audioTrack == null ? void 0 : audioTrack.container) === 'audio/mpeg') { 18598 checkTimestampOffset = !this.lastMpegAudioChunk || chunkMeta.id === 1 || this.lastMpegAudioChunk.sn !== chunkMeta.sn; 18599 this.lastMpegAudioChunk = chunkMeta; 18600 } 18601 const fragStart = frag.start; 18602 const operation = { 18603 execute: () => { 18604 chunkStats.executeStart = self.performance.now(); 18605 if (checkTimestampOffset) { 18606 const sb = this.sourceBuffer[type]; 18607 if (sb) { 18608 const delta = fragStart - sb.timestampOffset; 18609 if (Math.abs(delta) >= 0.1) { 18610 this.log(`Updating audio SourceBuffer timestampOffset to ${fragStart} (delta: ${delta}) sn: ${frag.sn})`); 18611 sb.timestampOffset = fragStart; 18612 } 18613 } 18614 } 18615 this.appendExecutor(data, type); 18616 }, 18617 onStart: () => { 18618 // logger.debug(`[buffer-controller]: ${type} SourceBuffer updatestart`); 18619 }, 18620 onComplete: () => { 18621 // logger.debug(`[buffer-controller]: ${type} SourceBuffer updateend`); 18622 const end = self.performance.now(); 18623 chunkStats.executeEnd = chunkStats.end = end; 18624 if (fragBuffering.first === 0) { 18625 fragBuffering.first = end; 18626 } 18627 if (partBuffering && partBuffering.first === 0) { 18628 partBuffering.first = end; 18629 } 18630 const { 18631 sourceBuffer 18632 } = this; 18633 const timeRanges = {}; 18634 for (const type in sourceBuffer) { 18635 timeRanges[type] = BufferHelper.getBuffered(sourceBuffer[type]); 18636 } 18637 this.appendErrors[type] = 0; 18638 if (type === 'audio' || type === 'video') { 18639 this.appendErrors.audiovideo = 0; 18640 } else { 18641 this.appendErrors.audio = 0; 18642 this.appendErrors.video = 0; 18643 } 18644 this.hls.trigger(Events.BUFFER_APPENDED, { 18645 type, 18646 frag, 18647 part, 18648 chunkMeta, 18649 parent: frag.type, 18650 timeRanges 18651 }); 18652 }, 18653 onError: error => { 18654 // in case any error occured while appending, put back segment in segments table 18655 const event = { 18656 type: ErrorTypes.MEDIA_ERROR, 18657 parent: frag.type, 18658 details: ErrorDetails.BUFFER_APPEND_ERROR, 18659 sourceBufferName: type, 18660 frag, 18661 part, 18662 chunkMeta, 18663 error, 18664 err: error, 18665 fatal: false 18666 }; 18667 if (error.code === DOMException.QUOTA_EXCEEDED_ERR) { 18668 // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror 18669 // let's stop appending any segments, and report BUFFER_FULL_ERROR error 18670 event.details = ErrorDetails.BUFFER_FULL_ERROR; 18671 } else { 18672 const appendErrorCount = ++this.appendErrors[type]; 18673 event.details = ErrorDetails.BUFFER_APPEND_ERROR; 18674 /* with UHD content, we could get loop of quota exceeded error until 18675 browser is able to evict some data from sourcebuffer. Retrying can help recover. 18676 */ 18677 this.warn(`Failed ${appendErrorCount}/${hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer`); 18678 if (appendErrorCount >= hls.config.appendErrorMaxRetry) { 18679 event.fatal = true; 18680 } 18681 } 18682 hls.trigger(Events.ERROR, event); 18683 } 18684 }; 18685 operationQueue.append(operation, type, !!this.pendingTracks[type]); 18686 } 18687 onBufferFlushing(event, data) { 18688 const { 18689 operationQueue 18690 } = this; 18691 const flushOperation = type => ({ 18692 execute: this.removeExecutor.bind(this, type, data.startOffset, data.endOffset), 18693 onStart: () => { 18694 // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); 18695 }, 18696 onComplete: () => { 18697 // logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); 18698 this.hls.trigger(Events.BUFFER_FLUSHED, { 18699 type 18700 }); 18701 }, 18702 onError: error => { 18703 this.warn(`Failed to remove from ${type} SourceBuffer`, error); 18704 } 18705 }); 18706 if (data.type) { 18707 operationQueue.append(flushOperation(data.type), data.type); 18708 } else {
18709 this.getSourceBufferTypes().forEach(type => { 18710 operationQueue.append(flushOperation(type), type); 18711 }); 18712 } 18713 } 18714 onFragParsed(event, data) { 18715 const { 18716 frag, 18717 part 18718 } = data; 18719 const buffersAppendedTo = []; 18720 const elementaryStreams = part ? part.elementaryStreams : frag.elementaryStreams; 18721 if (elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO]) { 18722 buffersAppendedTo.push('audiovideo'); 18723 } else { 18724 if (elementaryStreams[ElementaryStreamTypes.AUDIO]) { 18725 buffersAppendedTo.push('audio'); 18726 } 18727 if (elementaryStreams[ElementaryStreamTypes.VIDEO]) { 18728 buffersAppendedTo.push('video'); 18729 } 18730 } 18731 const onUnblocked = () => { 18732 const now = self.performance.now(); 18733 frag.stats.buffering.end = now; 18734 if (part) { 18735 part.stats.buffering.end = now; 18736 } 18737 const stats = part ? part.stats : frag.stats; 18738 this.hls.trigger(Events.FRAG_BUFFERED, { 18739 frag, 18740 part, 18741 stats, 18742 id: frag.type 18743 }); 18744 }; 18745 if (buffersAppendedTo.length === 0) { 18746 this.warn(`Fragments must have at least one ElementaryStreamType set. type: ${frag.type} level: ${frag.level} sn: ${frag.sn}`); 18747 } 18748 this.blockBuffers(onUnblocked, buffersAppendedTo); 18749 } 18750 onFragChanged(event, data) { 18751 this.trimBuffers(); 18752 } 18753 18754 // on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos() 18755 // an undefined data.type will mark all buffers as EOS. 18756 onBufferEos(event, data) { 18757 const ended = this.getSourceBufferTypes().reduce((acc, type) => { 18758 const sb = this.sourceBuffer[type]; 18759 if (sb && (!data.type || data.type === type)) { 18760 sb.ending = true; 18761 if (!sb.ended) { 18762 sb.ended = true; 18763 this.log(`${type} sourceBuffer now EOS`); 18764 } 18765 } 18766 return acc && !!(!sb || sb.ended); 18767 }, true); 18768 if (ended) { 18769 this.log(`Queueing mediaSource.endOfStream()`); 18770 this.blockBuffers(() => {
18771 this.getSourceBufferTypes().forEach(type => { 18772 const sb = this.sourceBuffer[type]; 18773 if (sb) { 18774 sb.ending = false; 18775 } 18776 }); 18777 const { 18778 mediaSource 18779 } = this; 18780 if (!mediaSource || mediaSource.readyState !== 'open') { 18781 if (mediaSource) { 18782 this.log(`Could not call mediaSource.endOfStream(). mediaSource.readyState: ${mediaSource.readyState}`); 18783 } 18784 return; 18785 } 18786 this.log(`Calling mediaSource.endOfStream()`); 18787 // Allow this to throw and be caught by the enqueueing function 18788 mediaSource.endOfStream(); 18789 }); 18790 } 18791 } 18792 onLevelUpdated(event, { 18793 details 18794 }) { 18795 if (!details.fragments.length) { 18796 return; 18797 } 18798 this.details = details; 18799 if (this.getSourceBufferTypes().length) { 18800 this.blockBuffers(this.updateMediaElementDuration.bind(this)); 18801 } else { 18802 this.updateMediaElementDuration(); 18803 } 18804 } 18805 trimBuffers() { 18806 const { 18807 hls, 18808 details, 18809 media 18810 } = this; 18811 if (!media || details === null) { 18812 return; 18813 } 18814 const sourceBufferTypes = this.getSourceBufferTypes(); 18815 if (!sourceBufferTypes.length) { 18816 return; 18817 } 18818 const config = hls.config; 18819 const currentTime = media.currentTime; 18820 const targetDuration = details.levelTargetDuration; 18821 18822 // Support for deprecated liveBackBufferLength 18823 const backBufferLength = details.live && config.liveBackBufferLength !== null ? config.liveBackBufferLength : config.backBufferLength; 18824 if (isFiniteNumber(backBufferLength) && backBufferLength > 0) { 18825 const maxBackBufferLength = Math.max(backBufferLength, targetDuration); 18826 const targetBackBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration - maxBackBufferLength; 18827 this.flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition); 18828 } 18829 if (isFiniteNumber(config.frontBufferFlushThreshold) && config.frontBufferFlushThreshold > 0) { 18830 const frontBufferLength = Math.max(config.maxBufferLength, config.frontBufferFlushThreshold); 18831 const maxFrontBufferLength = Math.max(frontBufferLength, targetDuration); 18832 const targetFrontBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration + maxFrontBufferLength; 18833 this.flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition); 18834 } 18835 } 18836 flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition) { 18837 const { 18838 details, 18839 sourceBuffer 18840 } = this; 18841 const sourceBufferTypes = this.getSourceBufferTypes(); 18842 sourceBufferTypes.forEach(type => { 18843 const sb = sourceBuffer[type]; 18844 if (sb) { 18845 const buffered = BufferHelper.getBuffered(sb); 18846 // when target buffer start exceeds actual buffer start 18847 if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) { 18848 this.hls.trigger(Events.BACK_BUFFER_REACHED, { 18849 bufferEnd: targetBackBufferPosition 18850 }); 18851 18852 // Support for deprecated event: 18853 if (details != null && details.live) { 18854 this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, { 18855 bufferEnd: targetBackBufferPosition 18856 }); 18857 } else if (sb.ended && buffered.end(buffered.length - 1) - currentTime < targetDuration * 2) { 18858 this.log(`Cannot flush ${type} back buffer while SourceBuffer is in ended state`); 18859 return; 18860 } 18861 this.hls.trigger(Events.BUFFER_FLUSHING, { 18862 startOffset: 0, 18863 endOffset: targetBackBufferPosition, 18864 type 18865 }); 18866 } 18867 } 18868 }); 18869 } 18870 flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition) { 18871 const { 18872 sourceBuffer 18873 } = this; 18874 const sourceBufferTypes = this.getSourceBufferTypes(); 18875 sourceBufferTypes.forEach(type => { 18876 const sb = sourceBuffer[type]; 18877 if (sb) { 18878 const buffered = BufferHelper.getBuffered(sb); 18879 const numBufferedRanges = buffered.length; 18880 // The buffer is either empty or contiguous 18881 if (numBufferedRanges < 2) { 18882 return; 18883 } 18884 const bufferStart = buffered.start(numBufferedRanges - 1); 18885 const bufferEnd = buffered.end(numBufferedRanges - 1); 18886 // No flush if we can tolerate the current buffer length or the current buffer range we would flush is contiguous with current position 18887 if (targetFrontBufferPosition > bufferStart || currentTime >= bufferStart && currentTime <= bufferEnd) { 18888 return; 18889 } else if (sb.ended && currentTime - bufferEnd < 2 * targetDuration) { 18890 this.log(`Cannot flush ${type} front buffer while SourceBuffer is in ended state`); 18891 return; 18892 } 18893 this.hls.trigger(Events.BUFFER_FLUSHING, { 18894 startOffset: bufferStart, 18895 endOffset: Infinity, 18896 type 18897 }); 18898 } 18899 }); 18900 } 18901 18902 /** 18903 * Update Media Source duration to current level duration or override to Infinity if configuration parameter 18904 * 'liveDurationInfinity` is set to `true` 18905 * More details: https://github.com/video-dev/hls.js/issues/355 18906 */ 18907 updateMediaElementDuration() { 18908 if (!this.details || !this.media || !this.mediaSource || this.mediaSource.readyState !== 'open') { 18909 return; 18910 } 18911 const { 18912 details, 18913 hls, 18914 media, 18915 mediaSource 18916 } = this; 18917 const levelDuration = details.fragments[0].start + details.totalduration; 18918 const mediaDuration = media.duration; 18919 const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : 0; 18920 if (details.live && hls.config.liveDurationInfinity) { 18921 // Override duration to Infinity 18922 mediaSource.duration = Infinity; 18923 this.updateSeekableRange(details); 18924 } else if (levelDuration > msDuration && levelDuration > mediaDuration || !isFiniteNumber(mediaDuration)) { 18925 // levelDuration was the last value we set. 18926 // not using mediaSource.duration as the browser may tweak this value 18927 // only update Media Source duration if its value increase, this is to avoid 18928 // flushing already buffered portion when switching between quality level 18929 this.log(`Updating Media Source duration to ${levelDuration.toFixed(3)}`); 18930 mediaSource.duration = levelDuration; 18931 } 18932 } 18933 updateSeekableRange(levelDetails) { 18934 const mediaSource = this.mediaSource; 18935 const fragments = levelDetails.fragments; 18936 const len = fragments.length; 18937 if (len && levelDetails.live && mediaSource != null && mediaSource.setLiveSeekableRange) { 18938 const start = Math.max(0, fragments[0].start); 18939 const end = Math.max(start, start + levelDetails.totalduration); 18940 this.log(`Media Source duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`); 18941 mediaSource.setLiveSeekableRange(start, end); 18942 } 18943 } 18944 checkPendingTracks() { 18945 const { 18946 bufferCodecEventsExpected, 18947 operationQueue, 18948 pendingTracks 18949 } = this; 18950 18951 // Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once.
18952 // This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after 18953 // data has been appended to existing ones. 18954 // 2 tracks is the max (one for audio, one for video). If we've reach this max go ahead and create the buffers. 18955 const pendingTracksCount = Object.keys(pendingTracks).length; 18956 if (pendingTracksCount && (!bufferCodecEventsExpected || pendingTracksCount === 2 || 'audiovideo' in pendingTracks)) { 18957 // ok, let's create them now ! 18958 this.createSourceBuffers(pendingTracks); 18959 this.pendingTracks = {}; 18960 // append any pending segments now ! 18961 const buffers = this.getSourceBufferTypes(); 18962 if (buffers.length) { 18963 this.hls.trigger(Events.BUFFER_CREATED, { 18964 tracks: this.tracks 18965 }); 18966 buffers.forEach(type => { 18967 operationQueue.executeNext(type); 18968 }); 18969 } else { 18970 const error = new Error('could not create source buffer for media codec(s)'); 18971 this.hls.trigger(Events.ERROR, { 18972 type: ErrorTypes.MEDIA_ERROR, 18973 details: ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR, 18974 fatal: true, 18975 error, 18976 reason: error.message 18977 }); 18978 } 18979 } 18980 } 18981 createSourceBuffers(tracks) { 18982 const { 18983 sourceBuffer, 18984 mediaSource 18985 } = this; 18986 if (!mediaSource) { 18987 throw Error('createSourceBuffers called when mediaSource was null'); 18988 } 18989 for (const trackName in tracks) { 18990 if (!sourceBuffer[trackName]) { 18991 var _track$levelCodec; 18992 const track = tracks[trackName]; 18993 if (!track) { 18994 throw Error(`source buffer exists for track ${trackName}, however track does not`); 18995 } 18996 // use levelCodec as first priority unless it contains multiple comma-separated codec values 18997 let codec = ((_track$levelCodec = track.levelCodec) == null ? void 0 : _track$levelCodec.indexOf(',')) === -1 ? track.levelCodec : track.codec; 18998 if (codec) { 18999 if (trackName.slice(0, 5) === 'audio') { 19000 codec = getCodecCompatibleName(codec, this.appendSource); 19001 } 19002 } 19003 const mimeType = `${track.container};codecs=${codec}`; 19004 this.log(`creating sourceBuffer(${mimeType})`); 19005 try { 19006 const sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType); 19007 const sbName = trackName; 19008 this.addBufferListener(sbName, 'updatestart', this._onSBUpdateStart); 19009 this.addBufferListener(sbName, 'updateend', this._onSBUpdateEnd); 19010 this.addBufferListener(sbName, 'error', this._onSBUpdateError); 19011 // ManagedSourceBuffer bufferedchange event 19012 if (this.appendSource) { 19013 this.addBufferListener(sbName, 'bufferedchange', (type, event) => { 19014 // If media was ejected check for a change. Added ranges are redundant with changes on 'updateend' event. 19015 const removedRanges = event.removedRanges; 19016 if (removedRanges != null && removedRanges.length) { 19017 this.hls.trigger(Events.BUFFER_FLUSHED, { 19018 type: trackName 19019 }); 19020 } 19021 }); 19022 } 19023 this.tracks[trackName] = { 19024 buffer: sb, 19025 codec: codec, 19026 container: track.container, 19027 levelCodec: track.levelCodec, 19028 metadata: track.metadata, 19029 id: track.id 19030 }; 19031 } catch (err) { 19032 this.error(`error while trying to add sourceBuffer: ${err.message}`); 19033 this.hls.trigger(Events.ERROR, { 19034 type: ErrorTypes.MEDIA_ERROR, 19035 details: ErrorDetails.BUFFER_ADD_CODEC_ERROR, 19036 fatal: false, 19037 error: err, 19038 sourceBufferName: trackName, 19039 mimeType: mimeType 19040 }); 19041 } 19042 } 19043 } 19044 } 19045 get mediaSrc() { 19046 var _this$media; 19047 const media = ((_this$media = this.media) == null ? void 0 : _this$media.firstChild) || this.media; 19048 return media == null ? void 0 : media.src; 19049 } 19050 _onSBUpdateStart(type) { 19051 const { 19052 operationQueue 19053 } = this; 19054 const operation = operationQueue.current(type); 19055 operation.onStart(); 19056 } 19057 _onSBUpdateEnd(type) { 19058 var _this$mediaSource2; 19059 if (((_this$mediaSource2 = this.mediaSource) == null ? void 0 : _this$mediaSource2.readyState) === 'closed') { 19060 this.resetBuffer(type); 19061 return; 19062 } 19063 const { 19064 operationQueue 19065 } = this; 19066 const operation = operationQueue.current(type); 19067 operation.onComplete(); 19068 operationQueue.shiftAndExecuteNext(type); 19069 } 19070 _onSBUpdateError(type, event) { 19071 var _this$mediaSource3; 19072 const error = new Error(`${type} SourceBuffer error. MediaSource readyState: ${(_this$mediaSource3 = this.mediaSource) == null ? void 0 : _this$mediaSource3.readyState}`); 19073 this.error(`${error}`, event); 19074 // according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error 19075 // SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event 19076 this.hls.trigger(Events.ERROR, { 19077 type: ErrorTypes.MEDIA_ERROR, 19078 details: ErrorDetails.BUFFER_APPENDING_ERROR, 19079 sourceBufferName: type, 19080 error, 19081 fatal: false 19082 }); 19083 // updateend is always fired after error, so we'll allow that to shift the current operation off of the queue 19084 const operation = this.operationQueue.current(type); 19085 if (operation) { 19086 operation.onError(error); 19087 } 19088 } 19089 19090 // This method must result in an updateend event; if remove is not called, _onSBUpdateEnd must be called manually 19091 removeExecutor(type, startOffset, endOffset) { 19092 const { 19093 media, 19094 mediaSource, 19095 operationQueue, 19096 sourceBuffer 19097 } = this; 19098 const sb = sourceBuffer[type]; 19099 if (!media || !mediaSource || !sb) { 19100 this.warn(`Attempting to remove from the ${type} SourceBuffer, but it does not exist`); 19101 operationQueue.shiftAndExecuteNext(type); 19102 return; 19103 } 19104 const mediaDuration = isFiniteNumber(media.duration) ? media.duration : Infinity; 19105 const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : Infinity; 19106 const removeStart = Math.max(0, startOffset); 19107 const removeEnd = Math.min(endOffset, mediaDuration, msDuration); 19108 if (removeEnd > removeStart && (!sb.ending || sb.ended)) { 19109 sb.ended = false;
19110 this.log(`Removing [${removeStart},${removeEnd}] from the ${type} SourceBuffer`); 19111 sb.remove(removeStart, removeEnd); 19112 } else { 19113 // Cycle the queue 19114 operationQueue.shiftAndExecuteNext(type); 19115 } 19116 } 19117 19118 // This method must result in an updateend event; if append is not called, _onSBUpdateEnd must be called manually 19119 appendExecutor(data, type) { 19120 const sb = this.sourceBuffer[type]; 19121 if (!sb) { 19122 if (!this.pendingTracks[type]) { 19123 throw new Error(`Attempting to append to the ${type} SourceBuffer, but it does not exist`); 19124 } 19125 return; 19126 } 19127 sb.ended = false; 19128 sb.appendBuffer(data); 19129 } 19130 19131 // Enqueues an operation to each SourceBuffer queue which, upon execution, resolves a promise. When all promises 19132 // resolve, the onUnblocked function is executed. Functions calling this method do not need to unblock the queue 19133 // upon completion, since we already do it here 19134 blockBuffers(onUnblocked, buffers = this.getSourceBufferTypes()) { 19135 if (!buffers.length) { 19136 this.log('Blocking operation requested, but no SourceBuffers exist'); 19137 Promise.resolve().then(onUnblocked); 19138 return; 19139 } 19140 const { 19141 operationQueue 19142 } = this; 19143 19144 // logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`); 19145 const blockingOperations = buffers.map(type => operationQueue.appendBlocker(type)); 19146 Promise.all(blockingOperations).then(() => { 19147 // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`); 19148 onUnblocked(); 19149 buffers.forEach(type => { 19150 const sb = this.sourceBuffer[type]; 19151 // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to 19152 // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration) 19153 // While this is a workaround, it's probably useful to have around 19154 if (!(sb != null && sb.updating)) { 19155 operationQueue.shiftAndExecuteNext(type); 19156 } 19157 }); 19158 }); 19159 } 19160 getSourceBufferTypes() { 19161 return Object.keys(this.sourceBuffer); 19162 } 19163 addBufferListener(type, event, fn) { 19164 const buffer = this.sourceBuffer[type]; 19165 if (!buffer) { 19166 return; 19167 } 19168 const listener = fn.bind(this, type); 19169 this.listeners[type].push({ 19170 event, 19171 listener 19172 }); 19173 buffer.addEventListener(event, listener); 19174 } 19175 removeBufferListeners(type) { 19176 const buffer = this.sourceBuffer[type]; 19177 if (!buffer) { 19178 return; 19179 } 19180 this.listeners[type].forEach(l => { 19181 buffer.removeEventListener(l.event, l.listener); 19182 }); 19183 } 19184} 19185function removeSourceChildren(node) { 19186 const sourceChildren = node.querySelectorAll('source'); 19187 [].slice.call(sourceChildren).forEach(source => { 19188 node.removeChild(source); 19189 }); 19190} 19191function addSource(media, url) { 19192 const source = self.document.createElement('source'); 19193 source.type = 'video/mp4'; 19194 source.src = url; 19195 media.appendChild(source);
vendor: 14,510 bytes, lines 19195-19770
19195 19196} 19197 19198/** 19199 * 19200 * This code was ported from the dash.js project at: 19201 * https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js 19202 * https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2 19203 * 19204 * The original copyright appears below: 19205 * 19206 * The copyright in this software is being made available under the BSD License, 19207 * included below. This software may be subject to other third party and contributor 19208 * rights, including patent rights, and no such rights are granted under this license. 19209 * 19210 * Copyright (c) 2015-2016, DASH Industry Forum. 19211 * All rights reserved. 19212 * 19213 * Redistribution and use in source and binary forms, with or without modification, 19214 * are permitted provided that the following conditions are met: 19215 * 1. Redistributions of source code must retain the above copyright notice, this 19216 * list of conditions and the following disclaimer. 19217 * * Redistributions in binary form must reproduce the above copyright notice, 19218 * this list of conditions and the following disclaimer in the documentation and/or 19219 * other materials provided with the distribution. 19220 * 2. Neither the name of Dash Industry Forum nor the names of its 19221 * contributors may be used to endorse or promote products derived from this software 19222 * without specific prior written permission. 19223 * 19224 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY 19225 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19226 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19227 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19228 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19229 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 19230 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 19231 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 19232 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 19233 * POSSIBILITY OF SUCH DAMAGE. 19234 */ 19235/** 19236 * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes 19237 */ 19238 19239const specialCea608CharsCodes = { 19240 0x2a: 0xe1, 19241 // lowercase a, acute accent 19242 0x5c: 0xe9, 19243 // lowercase e, acute accent 19244 0x5e: 0xed, 19245 // lowercase i, acute accent 19246 0x5f: 0xf3, 19247 // lowercase o, acute accent 19248 0x60: 0xfa, 19249 // lowercase u, acute accent 19250 0x7b: 0xe7, 19251 // lowercase c with cedilla 19252 0x7c: 0xf7, 19253 // division symbol 19254 0x7d: 0xd1, 19255 // uppercase N tilde 19256 0x7e: 0xf1, 19257 // lowercase n tilde 19258 0x7f: 0x2588, 19259 // Full block 19260 // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 19261 // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F 19262 // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES 19263 0x80: 0xae, 19264 // Registered symbol (R) 19265 0x81: 0xb0, 19266 // degree sign 19267 0x82: 0xbd, 19268 // 1/2 symbol 19269 0x83: 0xbf, 19270 // Inverted (open) question mark 19271 0x84: 0x2122, 19272 // Trademark symbol (TM) 19273 0x85: 0xa2, 19274 // Cents symbol 19275 0x86: 0xa3, 19276 // Pounds sterling 19277 0x87: 0x266a, 19278 // Music 8'th note 19279 0x88: 0xe0, 19280 // lowercase a, grave accent 19281 0x89: 0x20, 19282 // transparent space (regular) 19283 0x8a: 0xe8, 19284 // lowercase e, grave accent 19285 0x8b: 0xe2, 19286 // lowercase a, circumflex accent 19287 0x8c: 0xea, 19288 // lowercase e, circumflex accent 19289 0x8d: 0xee, 19290 // lowercase i, circumflex accent 19291 0x8e: 0xf4, 19292 // lowercase o, circumflex accent 19293 0x8f: 0xfb, 19294 // lowercase u, circumflex accent 19295 // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 19296 // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F 19297 0x90: 0xc1, 19298 // capital letter A with acute 19299 0x91: 0xc9, 19300 // capital letter E with acute 19301 0x92: 0xd3, 19302 // capital letter O with acute 19303 0x93: 0xda, 19304 // capital letter U with acute 19305 0x94: 0xdc, 19306 // capital letter U with diaresis 19307 0x95: 0xfc, 19308 // lowercase letter U with diaeresis 19309 0x96: 0x2018, 19310 // opening single quote 19311 0x97: 0xa1, 19312 // inverted exclamation mark 19313 0x98: 0x2a, 19314 // asterisk 19315 0x99: 0x2019, 19316 // closing single quote 19317 0x9a: 0x2501, 19318 // box drawings heavy horizontal 19319 0x9b: 0xa9, 19320 // copyright sign 19321 0x9c: 0x2120, 19322 // Service mark 19323 0x9d: 0x2022, 19324 // (round) bullet 19325 0x9e: 0x201c, 19326 // Left double quotation mark 19327 0x9f: 0x201d, 19328 // Right double quotation mark 19329 0xa0: 0xc0, 19330 // uppercase A, grave accent 19331 0xa1: 0xc2, 19332 // uppercase A, circumflex 19333 0xa2: 0xc7, 19334 // uppercase C with cedilla 19335 0xa3: 0xc8, 19336 // uppercase E, grave accent 19337 0xa4: 0xca, 19338 // uppercase E, circumflex 19339 0xa5: 0xcb, 19340 // capital letter E with diaresis 19341 0xa6: 0xeb, 19342 // lowercase letter e with diaresis 19343 0xa7: 0xce, 19344 // uppercase I, circumflex 19345 0xa8: 0xcf, 19346 // uppercase I, with diaresis 19347 0xa9: 0xef, 19348 // lowercase i, with diaresis 19349 0xaa: 0xd4, 19350 // uppercase O, circumflex 19351 0xab: 0xd9, 19352 // uppercase U, grave accent 19353 0xac: 0xf9, 19354 // lowercase u, grave accent 19355 0xad: 0xdb, 19356 // uppercase U, circumflex 19357 0xae: 0xab, 19358 // left-pointing double angle quotation mark 19359 0xaf: 0xbb, 19360 // right-pointing double angle quotation mark 19361 // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 19362 // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F 19363 0xb0: 0xc3, 19364 // Uppercase A, tilde 19365 0xb1: 0xe3, 19366 // Lowercase a, tilde 19367 0xb2: 0xcd, 19368 // Uppercase I, acute accent 19369 0xb3: 0xcc, 19370 // Uppercase I, grave accent 19371 0xb4: 0xec, 19372 // Lowercase i, grave accent 19373 0xb5: 0xd2, 19374 // Uppercase O, grave accent 19375 0xb6: 0xf2, 19376 // Lowercase o, grave accent 19377 0xb7: 0xd5, 19378 // Uppercase O, tilde 19379 0xb8: 0xf5, 19380 // Lowercase o, tilde 19381 0xb9: 0x7b, 19382 // Open curly brace 19383 0xba: 0x7d, 19384 // Closing curly brace 19385 0xbb: 0x5c, 19386 // Backslash 19387 0xbc: 0x5e, 19388 // Caret 19389 0xbd: 0x5f, 19390 // Underscore 19391 0xbe: 0x7c, 19392 // Pipe (vertical line) 19393 0xbf: 0x223c, 19394 // Tilde operator 19395 0xc0: 0xc4, 19396 // Uppercase A, umlaut 19397 0xc1: 0xe4, 19398 // Lowercase A, umlaut 19399 0xc2: 0xd6, 19400 // Uppercase O, umlaut 19401 0xc3: 0xf6, 19402 // Lowercase o, umlaut 19403 0xc4: 0xdf, 19404 // Esszett (sharp S) 19405 0xc5: 0xa5, 19406 // Yen symbol 19407 0xc6: 0xa4, 19408 // Generic currency sign 19409 0xc7: 0x2503, 19410 // Box drawings heavy vertical 19411 0xc8: 0xc5, 19412 // Uppercase A, ring 19413 0xc9: 0xe5, 19414 // Lowercase A, ring 19415 0xca: 0xd8, 19416 // Uppercase O, stroke 19417 0xcb: 0xf8, 19418 // Lowercase o, strok 19419 0xcc: 0x250f, 19420 // Box drawings heavy down and right 19421 0xcd: 0x2513, 19422 // Box drawings heavy down and left 19423 0xce: 0x2517, 19424 // Box drawings heavy up and right 19425 0xcf: 0x251b // Box drawings heavy up and left 19426}; 19427 19428/** 19429 * Utils 19430 */ 19431const getCharForByte = byte => String.fromCharCode(specialCea608CharsCodes[byte] || byte); 19432const NR_ROWS = 15; 19433const NR_COLS = 100; 19434// Tables to look up row from PAC data 19435const rowsLowCh1 = { 19436 0x11: 1, 19437 0x12: 3, 19438 0x15: 5, 19439 0x16: 7, 19440 0x17: 9, 19441 0x10: 11, 19442 0x13: 12, 19443 0x14: 14 19444}; 19445const rowsHighCh1 = { 19446 0x11: 2, 19447 0x12: 4, 19448 0x15: 6, 19449 0x16: 8, 19450 0x17: 10, 19451 0x13: 13, 19452 0x14: 15 19453}; 19454const rowsLowCh2 = { 19455 0x19: 1, 19456 0x1a: 3, 19457 0x1d: 5, 19458 0x1e: 7, 19459 0x1f: 9, 19460 0x18: 11, 19461 0x1b: 12, 19462 0x1c: 14 19463}; 19464const rowsHighCh2 = { 19465 0x19: 2, 19466 0x1a: 4, 19467 0x1d: 6, 19468 0x1e: 8, 19469 0x1f: 10, 19470 0x1b: 13, 19471 0x1c: 15 19472}; 19473const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent']; 19474class CaptionsLogger { 19475 constructor() { 19476 this.time = null; 19477 this.verboseLevel = 0; 19478 } 19479 log(severity, msg) { 19480 if (this.verboseLevel >= severity) { 19481 const m = typeof msg === 'function' ? msg() : msg; 19482 logger.log(`${this.time} [${severity}] ${m}`); 19483 } 19484 } 19485} 19486const numArrayToHexArray = function numArrayToHexArray(numArray) { 19487 const hexArray = []; 19488 for (let j = 0; j < numArray.length; j++) { 19489 hexArray.push(numArray[j].toString(16)); 19490 } 19491 return hexArray; 19492}; 19493class PenState { 19494 constructor() { 19495 this.foreground = 'white'; 19496 this.underline = false; 19497 this.italics = false; 19498 this.background = 'black'; 19499 this.flash = false; 19500 } 19501 reset() { 19502 this.foreground = 'white'; 19503 this.underline = false; 19504 this.italics = false; 19505 this.background = 'black'; 19506 this.flash = false; 19507 } 19508 setStyles(styles) { 19509 const attribs = ['foreground', 'underline', 'italics', 'background', 'flash']; 19510 for (let i = 0; i < attribs.length; i++) { 19511 const style = attribs[i]; 19512 if (styles.hasOwnProperty(style)) { 19513 this[style] = styles[style]; 19514 } 19515 } 19516 } 19517 isDefault() { 19518 return this.foreground === 'white' && !this.underline && !this.italics && this.background === 'black' && !this.flash; 19519 } 19520 equals(other) { 19521 return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash; 19522 } 19523 copy(newPenState) { 19524 this.foreground = newPenState.foreground; 19525 this.underline = newPenState.underline; 19526 this.italics = newPenState.italics; 19527 this.background = newPenState.background; 19528 this.flash = newPenState.flash; 19529 } 19530 toString() { 19531 return 'color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics + ', background=' + this.background + ', flash=' + this.flash; 19532 } 19533} 19534 19535/** 19536 * Unicode character with styling and background. 19537 * @constructor 19538 */ 19539class StyledUnicodeChar { 19540 constructor() { 19541 this.uchar = ' '; 19542 this.penState = new PenState(); 19543 } 19544 reset() { 19545 this.uchar = ' '; 19546 this.penState.reset(); 19547 } 19548 setChar(uchar, newPenState) { 19549 this.uchar = uchar; 19550 this.penState.copy(newPenState); 19551 } 19552 setPenState(newPenState) { 19553 this.penState.copy(newPenState); 19554 } 19555 equals(other) { 19556 return this.uchar === other.uchar && this.penState.equals(other.penState); 19557 } 19558 copy(newChar) { 19559 this.uchar = newChar.uchar; 19560 this.penState.copy(newChar.penState); 19561 } 19562 isEmpty() { 19563 return this.uchar === ' ' && this.penState.isDefault(); 19564 } 19565} 19566 19567/** 19568 * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar. 19569 * @constructor 19570 */ 19571class Row { 19572 constructor(logger) { 19573 this.chars = []; 19574 this.pos = 0; 19575 this.currPenState = new PenState(); 19576 this.cueStartTime = null; 19577 this.logger = void 0; 19578 for (let i = 0; i < NR_COLS; i++) { 19579 this.chars.push(new StyledUnicodeChar()); 19580 } 19581 this.logger = logger; 19582 } 19583 equals(other) { 19584 for (let i = 0; i < NR_COLS; i++) { 19585 if (!this.chars[i].equals(other.chars[i])) { 19586 return false; 19587 } 19588 } 19589 return true; 19590 } 19591 copy(other) { 19592 for (let i = 0; i < NR_COLS; i++) { 19593 this.chars[i].copy(other.chars[i]); 19594 } 19595 } 19596 isEmpty() { 19597 let empty = true; 19598 for (let i = 0; i < NR_COLS; i++) { 19599 if (!this.chars[i].isEmpty()) { 19600 empty = false; 19601 break; 19602 } 19603 } 19604 return empty; 19605 } 19606 19607 /** 19608 * Set the cursor to a valid column. 19609 */ 19610 setCursor(absPos) { 19611 if (this.pos !== absPos) { 19612 this.pos = absPos; 19613 } 19614 if (this.pos < 0) { 19615 this.logger.log(3, 'Negative cursor position ' + this.pos); 19616 this.pos = 0; 19617 } else if (this.pos > NR_COLS) { 19618 this.logger.log(3, 'Too large cursor position ' + this.pos); 19619 this.pos = NR_COLS; 19620 } 19621 } 19622 19623 /** 19624 * Move the cursor relative to current position. 19625 */ 19626 moveCursor(relPos) { 19627 const newPos = this.pos + relPos; 19628 if (relPos > 1) { 19629 for (let i = this.pos + 1; i < newPos + 1; i++) { 19630 this.chars[i].setPenState(this.currPenState); 19631 } 19632 } 19633 this.setCursor(newPos); 19634 } 19635 19636 /** 19637 * Backspace, move one step back and clear character. 19638 */ 19639 backSpace() { 19640 this.moveCursor(-1); 19641 this.chars[this.pos].setChar(' ', this.currPenState); 19642 } 19643 insertChar(byte) { 19644 if (byte >= 0x90) { 19645 // Extended char 19646 this.backSpace(); 19647 } 19648 const char = getCharForByte(byte); 19649 if (this.pos >= NR_COLS) { 19650 this.logger.log(0, () => 'Cannot insert ' + byte.toString(16) + ' (' + char + ') at position ' + this.pos + '. Skipping it!'); 19651 return; 19652 } 19653 this.chars[this.pos].setChar(char, this.currPenState); 19654 this.moveCursor(1); 19655 } 19656 clearFromPos(startPos) { 19657 let i; 19658 for (i = startPos; i < NR_COLS; i++) { 19659 this.chars[i].reset(); 19660 } 19661 } 19662 clear() { 19663 this.clearFromPos(0); 19664 this.pos = 0; 19665 this.currPenState.reset(); 19666 } 19667 clearToEndOfRow() { 19668 this.clearFromPos(this.pos); 19669 } 19670 getTextString() { 19671 const chars = []; 19672 let empty = true; 19673 for (let i = 0; i < NR_COLS; i++) { 19674 const char = this.chars[i].uchar; 19675 if (char !== ' ') { 19676 empty = false; 19677 } 19678 chars.push(char); 19679 } 19680 if (empty) { 19681 return ''; 19682 } else { 19683 return chars.join(''); 19684 } 19685 } 19686 setPenStyles(styles) { 19687 this.currPenState.setStyles(styles); 19688 const currChar = this.chars[this.pos]; 19689 currChar.setPenState(this.currPenState); 19690 } 19691} 19692 19693/** 19694 * Keep a CEA-608 screen of 32x15 styled characters 19695 * @constructor 19696 */ 19697class CaptionScreen { 19698 constructor(logger) { 19699 this.rows = []; 19700 this.currRow = NR_ROWS - 1; 19701 this.nrRollUpRows = null; 19702 this.lastOutputScreen = null; 19703 this.logger = void 0; 19704 for (let i = 0; i < NR_ROWS; i++) { 19705 this.rows.push(new Row(logger)); 19706 } 19707 this.logger = logger; 19708 } 19709 reset() { 19710 for (let i = 0; i < NR_ROWS; i++) { 19711 this.rows[i].clear(); 19712 } 19713 this.currRow = NR_ROWS - 1; 19714 } 19715 equals(other) { 19716 let equal = true; 19717 for (let i = 0; i < NR_ROWS; i++) { 19718 if (!this.rows[i].equals(other.rows[i])) { 19719 equal = false; 19720 break; 19721 } 19722 } 19723 return equal; 19724 } 19725 copy(other) { 19726 for (let i = 0; i < NR_ROWS; i++) { 19727 this.rows[i].copy(other.rows[i]); 19728 } 19729 } 19730 isEmpty() { 19731 let empty = true; 19732 for (let i = 0; i < NR_ROWS; i++) { 19733 if (!this.rows[i].isEmpty()) { 19734 empty = false; 19735 break; 19736 } 19737 } 19738 return empty; 19739 } 19740 backSpace() { 19741 const row = this.rows[this.currRow]; 19742 row.backSpace(); 19743 } 19744 clearToEndOfRow() { 19745 const row = this.rows[this.currRow]; 19746 row.clearToEndOfRow(); 19747 } 19748 19749 /** 19750 * Insert a character (without styling) in the current row. 19751 */ 19752 insertChar(char) { 19753 const row = this.rows[this.currRow]; 19754 row.insertChar(char); 19755 } 19756 setPen(styles) { 19757 const row = this.rows[this.currRow]; 19758 row.setPenStyles(styles); 19759 } 19760 moveCursor(relPos) { 19761 const row = this.rows[this.currRow]; 19762 row.moveCursor(relPos); 19763 } 19764 setCursor(absPos) { 19765 this.logger.log(2, 'setCursor: ' + absPos); 19766 const row = this.rows[this.currRow]; 19767 row.setCursor(absPos); 19768 } 19769 setPAC(pacData) { 19770 this.logger.log(2, () => 'pacData = ' +
vendor: 1,927 bytes, lines 19770-19821
19770JSON.stringify(pacData)); 19771 let newRow = pacData.row - 1; 19772 if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) { 19773 newRow = this.nrRollUpRows - 1; 19774 } 19775 19776 // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows 19777 if (this.nrRollUpRows && this.currRow !== newRow) { 19778 // clear all rows first 19779 for (let i = 0; i < NR_ROWS; i++) { 19780 this.rows[i].clear(); 19781 } 19782 19783 // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location 19784 // topRowIndex - the start of rows to copy (inclusive index) 19785 const topRowIndex = this.currRow + 1 - this.nrRollUpRows; 19786 // We only copy if the last position was already shown. 19787 // We use the cueStartTime value to check this. 19788 const lastOutputScreen = this.lastOutputScreen; 19789 if (lastOutputScreen) { 19790 const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime; 19791 const time = this.logger.time; 19792 if (prevLineTime !== null && time !== null && prevLineTime < time) { 19793 for (let i = 0; i < this.nrRollUpRows; i++) { 19794 this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]); 19795 } 19796 } 19797 } 19798 } 19799 this.currRow = newRow; 19800 const row = this.rows[this.currRow]; 19801 if (pacData.indent !== null) { 19802 const indent = pacData.indent; 19803 const prevPos = Math.max(indent - 1, 0); 19804 row.setCursor(pacData.indent); 19805 pacData.color = row.chars[prevPos].penState.foreground; 19806 } 19807 const styles = { 19808 foreground: pacData.color, 19809 underline: pacData.underline, 19810 italics: pacData.italics, 19811 background: 'black', 19812 flash: false 19813 }; 19814 this.setPen(styles); 19815 } 19816 19817 /** 19818 * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility). 19819 */ 19820 setBkgData(bkgData) { 19821 this.logger.log(2, () => 'bkgData = ' + JSON.
vendor: 7,079 bytes, lines 19821-20061
19821stringify(bkgData)); 19822 this.backSpace(); 19823 this.setPen(bkgData); 19824 this.insertChar(0x20); // Space 19825 } 19826 setRollUpRows(nrRows) { 19827 this.nrRollUpRows = nrRows; 19828 } 19829 rollUp() { 19830 if (this.nrRollUpRows === null) { 19831 this.logger.log(3, 'roll_up but nrRollUpRows not set yet'); 19832 return; // Not properly setup 19833 } 19834 this.logger.log(1, () => this.getDisplayText()); 19835 const topRowIndex = this.currRow + 1 - this.nrRollUpRows; 19836 const topRow = this.rows.splice(topRowIndex, 1)[0]; 19837 topRow.clear(); 19838 this.rows.splice(this.currRow, 0, topRow); 19839 this.logger.log(2, 'Rolling up'); 19840 // this.logger.log(VerboseLevel.TEXT, this.get_display_text()) 19841 } 19842 19843 /** 19844 * Get all non-empty rows with as unicode text. 19845 */ 19846 getDisplayText(asOneRow) { 19847 asOneRow = asOneRow || false; 19848 const displayText = []; 19849 let text = ''; 19850 let rowNr = -1; 19851 for (let i = 0; i < NR_ROWS; i++) { 19852 const rowText = this.rows[i].getTextString(); 19853 if (rowText) { 19854 rowNr = i + 1; 19855 if (asOneRow) { 19856 displayText.push('Row ' + rowNr + ": '" + rowText + "'"); 19857 } else { 19858 displayText.push(rowText.trim()); 19859 } 19860 } 19861 } 19862 if (displayText.length > 0) { 19863 if (asOneRow) { 19864 text = '[' + displayText.join(' | ') + ']'; 19865 } else { 19866 text = displayText.join('\n'); 19867 } 19868 } 19869 return text; 19870 } 19871 getTextAndFormat() { 19872 return this.rows; 19873 } 19874} 19875 19876// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT']; 19877 19878class Cea608Channel { 19879 constructor(channelNumber, outputFilter, logger) { 19880 this.chNr = void 0; 19881 this.outputFilter = void 0; 19882 this.mode = void 0; 19883 this.verbose = void 0; 19884 this.displayedMemory = void 0; 19885 this.nonDisplayedMemory = void 0; 19886 this.lastOutputScreen = void 0; 19887 this.currRollUpRow = void 0; 19888 this.writeScreen = void 0; 19889 this.cueStartTime = void 0; 19890 this.logger = void 0; 19891 this.chNr = channelNumber; 19892 this.outputFilter = outputFilter; 19893 this.mode = null; 19894 this.verbose = 0; 19895 this.displayedMemory = new CaptionScreen(logger); 19896 this.nonDisplayedMemory = new CaptionScreen(logger); 19897 this.lastOutputScreen = new CaptionScreen(logger); 19898 this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; 19899 this.writeScreen = this.displayedMemory; 19900 this.mode = null; 19901 this.cueStartTime = null; // Keeps track of where a cue started. 19902 this.logger = logger; 19903 } 19904 reset() { 19905 this.mode = null; 19906 this.displayedMemory.reset(); 19907 this.nonDisplayedMemory.reset(); 19908 this.lastOutputScreen.reset(); 19909 this.outputFilter.reset(); 19910 this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; 19911 this.writeScreen = this.displayedMemory; 19912 this.mode = null; 19913 this.cueStartTime = null; 19914 } 19915 getHandler() { 19916 return this.outputFilter; 19917 } 19918 setHandler(newHandler) { 19919 this.outputFilter = newHandler; 19920 } 19921 setPAC(pacData) { 19922 this.writeScreen.setPAC(pacData); 19923 } 19924 setBkgData(bkgData) { 19925 this.writeScreen.setBkgData(bkgData); 19926 } 19927 setMode(newMode) { 19928 if (newMode === this.mode) { 19929 return; 19930 } 19931 this.mode = newMode; 19932 this.logger.log(2, () => 'MODE=' + newMode); 19933 if (this.mode === 'MODE_POP-ON') { 19934 this.writeScreen = this.nonDisplayedMemory; 19935 } else { 19936 this.writeScreen = this.displayedMemory; 19937 this.writeScreen.reset(); 19938 } 19939 if (this.mode !== 'MODE_ROLL-UP') { 19940 this.displayedMemory.nrRollUpRows = null; 19941 this.nonDisplayedMemory.nrRollUpRows = null; 19942 } 19943 this.mode = newMode; 19944 } 19945 insertChars(chars) { 19946 for (let i = 0; i < chars.length; i++) { 19947 this.writeScreen.insertChar(chars[i]); 19948 } 19949 const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP'; 19950 this.logger.log(2, () => screen + ': ' + this.writeScreen.getDisplayText(true)); 19951 if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') { 19952 this.logger.log(1, () => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)); 19953 this.outputDataUpdate(); 19954 } 19955 } 19956 ccRCL() { 19957 // Resume Caption Loading (switch mode to Pop On) 19958 this.logger.log(2, 'RCL - Resume Caption Loading'); 19959 this.setMode('MODE_POP-ON'); 19960 } 19961 ccBS() { 19962 // BackSpace 19963 this.logger.log(2, 'BS - BackSpace'); 19964 if (this.mode === 'MODE_TEXT') { 19965 return; 19966 } 19967 this.writeScreen.backSpace(); 19968 if (this.writeScreen === this.displayedMemory) { 19969 this.outputDataUpdate(); 19970 } 19971 } 19972 ccAOF() { 19973 // Reserved (formerly Alarm Off) 19974 } 19975 ccAON() { 19976 // Reserved (formerly Alarm On) 19977 } 19978 ccDER() { 19979 // Delete to End of Row 19980 this.logger.log(2, 'DER- Delete to End of Row'); 19981 this.writeScreen.clearToEndOfRow(); 19982 this.outputDataUpdate(); 19983 } 19984 ccRU(nrRows) { 19985 // Roll-Up Captions-2,3,or 4 Rows 19986 this.logger.log(2, 'RU(' + nrRows + ') - Roll Up'); 19987 this.writeScreen = this.displayedMemory; 19988 this.setMode('MODE_ROLL-UP'); 19989 this.writeScreen.setRollUpRows(nrRows); 19990 } 19991 ccFON() { 19992 // Flash On 19993 this.logger.log(2, 'FON - Flash On'); 19994 this.writeScreen.setPen({ 19995 flash: true 19996 }); 19997 } 19998 ccRDC() { 19999 // Resume Direct Captioning (switch mode to PaintOn) 20000 this.logger.log(2, 'RDC - Resume Direct Captioning'); 20001 this.setMode('MODE_PAINT-ON'); 20002 } 20003 ccTR() { 20004 // Text Restart in text mode (not supported, however) 20005 this.logger.log(2, 'TR'); 20006 this.setMode('MODE_TEXT'); 20007 } 20008 ccRTD() { 20009 // Resume Text Display in Text mode (not supported, however) 20010 this.logger.log(2, 'RTD'); 20011 this.setMode('MODE_TEXT'); 20012 } 20013 ccEDM() { 20014 // Erase Displayed Memory 20015 this.logger.log(2, 'EDM - Erase Displayed Memory'); 20016 this.displayedMemory.reset(); 20017 this.outputDataUpdate(true); 20018 } 20019 ccCR() { 20020 // Carriage Return 20021 this.logger.log(2, 'CR - Carriage Return'); 20022 this.writeScreen.rollUp(); 20023 this.outputDataUpdate(true); 20024 } 20025 ccENM() { 20026 // Erase Non-Displayed Memory 20027 this.logger.log(2, 'ENM - Erase Non-displayed Memory'); 20028 this.nonDisplayedMemory.reset(); 20029 } 20030 ccEOC() { 20031 // End of Caption (Flip Memories) 20032 this.logger.log(2, 'EOC - End Of Caption'); 20033 if (this.mode === 'MODE_POP-ON') { 20034 const tmp = this.displayedMemory; 20035 this.displayedMemory = this.nonDisplayedMemory; 20036 this.nonDisplayedMemory = tmp; 20037 this.writeScreen = this.nonDisplayedMemory; 20038 this.logger.log(1, () => 'DISP: ' + this.displayedMemory.getDisplayText()); 20039 } 20040 this.outputDataUpdate(true); 20041 } 20042 ccTO(nrCols) { 20043 // Tab Offset 1,2, or 3 columns 20044 this.logger.log(2, 'TO(' + nrCols + ') - Tab Offset'); 20045 this.writeScreen.moveCursor(nrCols); 20046 } 20047 ccMIDROW(secondByte) { 20048 // Parse MIDROW command 20049 const styles = { 20050 flash: false 20051 }; 20052 styles.underline = secondByte % 2 === 1; 20053 styles.italics = secondByte >= 0x2e; 20054 if (!styles.italics) { 20055 const colorIndex = Math.floor(secondByte / 2) - 0x10; 20056 const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta']; 20057 styles.foreground = colors[colorIndex]; 20058 } else { 20059 styles.foreground = 'white'; 20060 } 20061 this.logger.log(2, 'MIDROW: ' +
20061JSON.
vendor: 10,155 bytes, lines 20061-20414
20061stringify(styles)); 20062 this.writeScreen.setPen(styles); 20063 } 20064 outputDataUpdate(dispatch = false) { 20065 const time = this.logger.time; 20066 if (time === null) { 20067 return; 20068 } 20069 if (this.outputFilter) { 20070 if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) { 20071 // Start of a new cue 20072 this.cueStartTime = time; 20073 } else { 20074 if (!this.displayedMemory.equals(this.lastOutputScreen)) { 20075 this.outputFilter.newCue(this.cueStartTime, time, this.lastOutputScreen); 20076 if (dispatch && this.outputFilter.dispatchCue) { 20077 this.outputFilter.dispatchCue(); 20078 } 20079 this.cueStartTime = this.displayedMemory.isEmpty() ? null : time; 20080 } 20081 } 20082 this.lastOutputScreen.copy(this.displayedMemory); 20083 } 20084 } 20085 cueSplitAtTime(t) { 20086 if (this.outputFilter) { 20087 if (!this.displayedMemory.isEmpty()) { 20088 if (this.outputFilter.newCue) { 20089 this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory); 20090 } 20091 this.cueStartTime = t; 20092 } 20093 } 20094 } 20095} 20096 20097// Will be 1 or 2 when parsing captions 20098 20099class Cea608Parser { 20100 constructor(field, out1, out2) { 20101 this.channels = void 0; 20102 this.currentChannel = 0; 20103 this.cmdHistory = createCmdHistory(); 20104 this.logger = void 0; 20105 const logger = this.logger = new CaptionsLogger(); 20106 this.channels = [null, new Cea608Channel(field, out1, logger), new Cea608Channel(field + 1, out2, logger)]; 20107 } 20108 getHandler(channel) { 20109 return this.channels[channel].getHandler(); 20110 } 20111 setHandler(channel, newHandler) { 20112 this.channels[channel].setHandler(newHandler); 20113 } 20114 20115 /** 20116 * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs. 20117 */ 20118 addData(time, byteList) { 20119 this.logger.time = time; 20120 for (let i = 0; i < byteList.length; i += 2) { 20121 const a = byteList[i] & 0x7f; 20122 const b = byteList[i + 1] & 0x7f; 20123 let cmdFound = false; 20124 let charsFound = null; 20125 if (a === 0 && b === 0) { 20126 continue; 20127 } else { 20128 this.logger.log(3, () => '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')'); 20129 } 20130 const cmdHistory = this.cmdHistory; 20131 const isControlCode = a >= 0x10 && a <= 0x1f; 20132 if (isControlCode) { 20133 // Skip redundant control codes 20134 if (hasCmdRepeated(a, b, cmdHistory)) { 20135 setLastCmd(null, null, cmdHistory); 20136 this.logger.log(3, () => 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped'); 20137 continue; 20138 } 20139 setLastCmd(a, b, this.cmdHistory); 20140 cmdFound = this.parseCmd(a, b); 20141 if (!cmdFound) { 20142 cmdFound = this.parseMidrow(a, b); 20143 } 20144 if (!cmdFound) { 20145 cmdFound = this.parsePAC(a, b); 20146 } 20147 if (!cmdFound) { 20148 cmdFound = this.parseBackgroundAttributes(a, b); 20149 } 20150 } else { 20151 setLastCmd(null, null, cmdHistory); 20152 } 20153 if (!cmdFound) { 20154 charsFound = this.parseChars(a, b); 20155 if (charsFound) { 20156 const currChNr = this.currentChannel; 20157 if (currChNr && currChNr > 0) { 20158 const channel = this.channels[currChNr]; 20159 channel.insertChars(charsFound); 20160 } else { 20161 this.logger.log(2, 'No channel found yet. TEXT-MODE?'); 20162 } 20163 } 20164 } 20165 if (!cmdFound && !charsFound) { 20166 this.logger.log(2, () => "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]])); 20167 } 20168 } 20169 } 20170 20171 /** 20172 * Parse Command. 20173 * @returns True if a command was found 20174 */ 20175 parseCmd(a, b) { 20176 const cond1 = (a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) && b >= 0x20 && b <= 0x2f; 20177 const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23; 20178 if (!(cond1 || cond2)) { 20179 return false; 20180 } 20181 const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2; 20182 const channel = this.channels[chNr]; 20183 if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) { 20184 if (b === 0x20) { 20185 channel.ccRCL(); 20186 } else if (b === 0x21) { 20187 channel.ccBS(); 20188 } else if (b === 0x22) { 20189 channel.ccAOF(); 20190 } else if (b === 0x23) { 20191 channel.ccAON(); 20192 } else if (b === 0x24) { 20193 channel.ccDER(); 20194 } else if (b === 0x25) { 20195 channel.ccRU(2); 20196 } else if (b === 0x26) { 20197 channel.ccRU(3); 20198 } else if (b === 0x27) { 20199 channel.ccRU(4); 20200 } else if (b === 0x28) { 20201 channel.ccFON(); 20202 } else if (b === 0x29) { 20203 channel.ccRDC(); 20204 } else if (b === 0x2a) { 20205 channel.ccTR(); 20206 } else if (b === 0x2b) { 20207 channel.ccRTD(); 20208 } else if (b === 0x2c) { 20209 channel.ccEDM(); 20210 } else if (b === 0x2d) { 20211 channel.ccCR(); 20212 } else if (b === 0x2e) { 20213 channel.ccENM(); 20214 } else if (b === 0x2f) { 20215 channel.ccEOC(); 20216 } 20217 } else { 20218 // a == 0x17 || a == 0x1F 20219 channel.ccTO(b - 0x20); 20220 } 20221 this.currentChannel = chNr; 20222 return true; 20223 } 20224 20225 /** 20226 * Parse midrow styling command 20227 */ 20228 parseMidrow(a, b) { 20229 let chNr = 0; 20230 if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) { 20231 if (a === 0x11) { 20232 chNr = 1; 20233 } else { 20234 chNr = 2; 20235 } 20236 if (chNr !== this.currentChannel) { 20237 this.logger.log(0, 'Mismatch channel in midrow parsing'); 20238 return false; 20239 } 20240 const channel = this.channels[chNr]; 20241 if (!channel) { 20242 return false; 20243 } 20244 channel.ccMIDROW(b); 20245 this.logger.log(3, () => 'MIDROW (' + numArrayToHexArray([a, b]) + ')'); 20246 return true; 20247 } 20248 return false; 20249 } 20250 20251 /** 20252 * Parse Preable Access Codes (Table 53). 20253 * @returns {Boolean} Tells if PAC found 20254 */ 20255 parsePAC(a, b) { 20256 let row; 20257 const case1 = (a >= 0x11 && a <= 0x17 || a >= 0x19 && a <= 0x1f) && b >= 0x40 && b <= 0x7f; 20258 const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f; 20259 if (!(case1 || case2)) { 20260 return false; 20261 } 20262 const chNr = a <= 0x17 ? 1 : 2; 20263 if (b >= 0x40 && b <= 0x5f) { 20264 row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a]; 20265 } else { 20266 // 0x60 <= b <= 0x7F 20267 row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a]; 20268 } 20269 const channel = this.channels[chNr]; 20270 if (!channel) { 20271 return false; 20272 } 20273 channel.setPAC(this.interpretPAC(row, b)); 20274 this.currentChannel = chNr; 20275 return true; 20276 } 20277 20278 /** 20279 * Interpret the second byte of the pac, and return the information. 20280 * @returns pacData with style parameters 20281 */ 20282 interpretPAC(row, byte) { 20283 let pacIndex; 20284 const pacData = { 20285 color: null, 20286 italics: false, 20287 indent: null, 20288 underline: false, 20289 row: row 20290 }; 20291 if (byte > 0x5f) { 20292 pacIndex = byte - 0x60; 20293 } else { 20294 pacIndex = byte - 0x40; 20295 } 20296 pacData.underline = (pacIndex & 1) === 1; 20297 if (pacIndex <= 0xd) { 20298 pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)]; 20299 } else if (pacIndex <= 0xf) { 20300 pacData.italics = true; 20301 pacData.color = 'white'; 20302 } else { 20303 pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4; 20304 } 20305 return pacData; // Note that row has zero offset. The spec uses 1. 20306 } 20307 20308 /** 20309 * Parse characters. 20310 * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise. 20311 */ 20312 parseChars(a, b) { 20313 let channelNr; 20314 let charCodes = null; 20315 let charCode1 = null; 20316 if (a >= 0x19) { 20317 channelNr = 2; 20318 charCode1 = a - 8; 20319 } else { 20320 channelNr = 1; 20321 charCode1 = a; 20322 } 20323 if (charCode1 >= 0x11 && charCode1 <= 0x13) { 20324 // Special character 20325 let oneCode; 20326 if (charCode1 === 0x11) { 20327 oneCode = b + 0x50; 20328 } else if (charCode1 === 0x12) { 20329 oneCode = b + 0x70; 20330 } else { 20331 oneCode = b + 0x90; 20332 } 20333 this.logger.log(2, () => "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr); 20334 charCodes = [oneCode]; 20335 } else if (a >= 0x20 && a <= 0x7f) { 20336 charCodes = b === 0 ? [a] : [a, b]; 20337 } 20338 if (charCodes) { 20339 this.logger.log(3, () => 'Char codes = ' + numArrayToHexArray(charCodes).join(',')); 20340 } 20341 return charCodes; 20342 } 20343 20344 /** 20345 * Parse extended background attributes as well as new foreground color black. 20346 * @returns True if background attributes are found 20347 */ 20348 parseBackgroundAttributes(a, b) { 20349 const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f; 20350 const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f; 20351 if (!(case1 || case2)) { 20352 return false; 20353 } 20354 let index; 20355 const bkgData = {}; 20356 if (a === 0x10 || a === 0x18) { 20357 index = Math.floor((b - 0x20) / 2); 20358 bkgData.background = backgroundColors[index]; 20359 if (b % 2 === 1) { 20360 bkgData.background = bkgData.background + '_semi'; 20361 } 20362 } else if (b === 0x2d) { 20363 bkgData.background = 'transparent'; 20364 } else { 20365 bkgData.foreground = 'black'; 20366 if (b === 0x2f) { 20367 bkgData.underline = true; 20368 } 20369 } 20370 const chNr = a <= 0x17 ? 1 : 2; 20371 const channel = this.channels[chNr]; 20372 channel.setBkgData(bkgData); 20373 return true; 20374 } 20375 20376 /** 20377 * Reset state of parser and its channels. 20378 */ 20379 reset() { 20380 for (let i = 0; i < Object.keys(this.channels).length; i++) { 20381 const channel = this.channels[i]; 20382 if (channel) { 20383 channel.reset(); 20384 } 20385 } 20386 setLastCmd(null, null, this.cmdHistory); 20387 } 20388 20389 /** 20390 * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty. 20391 */ 20392 cueSplitAtTime(t) { 20393 for (let i = 0; i < this.channels.length; i++) { 20394 const channel = this.channels[i]; 20395 if (channel) { 20396 channel.cueSplitAtTime(t); 20397 } 20398 } 20399 } 20400} 20401function setLastCmd(a, b, cmdHistory) { 20402 cmdHistory.a = a; 20403 cmdHistory.b = b; 20404} 20405function hasCmdRepeated(a, b, cmdHistory) { 20406 return cmdHistory.a === a && cmdHistory.b === b; 20407} 20408function createCmdHistory() { 20409 return { 20410 a: null, 20411 b: null 20412 }; 20413} 20414
20415class OutputFilter { 20416 constructor(timelineController, trackName) { 20417 this.timelineController = void 0; 20418 this.cueRanges = []; 20419 this.trackName = void 0; 20420 this.startTime = null; 20421 this.endTime = null; 20422 this.screen = null; 20423 this.timelineController = timelineController; 20424 this.trackName = trackName; 20425 } 20426 dispatchCue() { 20427 if (this.startTime === null) { 20428 return; 20429 } 20430 this.timelineController.addCues(this.trackName, this.startTime, this.endTime, this.screen, this.cueRanges); 20431 this.startTime = null; 20432 } 20433 newCue(startTime, endTime, screen) { 20434 if (this.startTime === null || this.startTime > startTime) { 20435 this.startTime = startTime; 20436 } 20437 this.endTime = endTime; 20438 this.screen = screen; 20439 this.timelineController.createCaptionsTrack(this.trackName); 20440 } 20441 reset() { 20442 this.cueRanges = []; 20443 this.startTime = null;
vendor: 7,872 bytes, lines 20443-20720
20443 20444 } 20445} 20446 20447/** 20448 * Copyright 2013 vtt.js Contributors 20449 * 20450 * Licensed under the Apache License, Version 2.0 (the 'License'); 20451 * you may not use this file except in compliance with the License. 20452 * You may obtain a copy of the License at 20453 * 20454 * http://www.apache.org/licenses/LICENSE-2.0 20455 * 20456 * Unless required by applicable law or agreed to in writing, software 20457 * distributed under the License is distributed on an 'AS IS' BASIS, 20458 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20459 * See the License for the specific language governing permissions and 20460 * limitations under the License. 20461 */ 20462 20463var VTTCue = (function () { 20464 if (optionalSelf != null && optionalSelf.VTTCue) { 20465 return self.VTTCue; 20466 } 20467 const AllowedDirections = ['', 'lr', 'rl']; 20468 const AllowedAlignments = ['start', 'middle', 'end', 'left', 'right']; 20469 function isAllowedValue(allowed, value) { 20470 if (typeof value !== 'string') { 20471 return false; 20472 } 20473 // necessary for assuring the generic conforms to the Array interface 20474 if (!Array.isArray(allowed)) { 20475 return false; 20476 } 20477 // reset the type so that the next narrowing works well 20478 const lcValue = value.toLowerCase(); 20479 // use the allow list to narrow the type to a specific subset of strings 20480 if (~allowed.indexOf(lcValue)) { 20481 return lcValue; 20482 } 20483 return false; 20484 } 20485 function findDirectionSetting(value) { 20486 return isAllowedValue(AllowedDirections, value); 20487 } 20488 function findAlignSetting(value) { 20489 return isAllowedValue(AllowedAlignments, value); 20490 } 20491 function extend(obj, ...rest) { 20492 let i = 1; 20493 for (; i < arguments.length; i++) { 20494 const cobj = arguments[i]; 20495 for (const p in cobj) { 20496 obj[p] = cobj[p]; 20497 } 20498 } 20499 return obj; 20500 } 20501 function VTTCue(startTime, endTime, text) { 20502 const cue = this; 20503 const baseObj = { 20504 enumerable: true 20505 }; 20506 /** 20507 * Shim implementation specific properties. These properties are not in 20508 * the spec. 20509 */ 20510 20511 // Lets us know when the VTTCue's data has changed in such a way that we need 20512 // to recompute its display state. This lets us compute its display state 20513 // lazily. 20514 cue.hasBeenReset = false; 20515 20516 /** 20517 * VTTCue and TextTrackCue properties 20518 * http://dev.w3.org/html5/webvtt/#vttcue-interface 20519 */ 20520 20521 let _id = ''; 20522 let _pauseOnExit = false; 20523 let _startTime = startTime; 20524 let _endTime = endTime; 20525 let _text = text; 20526 let _region = null; 20527 let _vertical = ''; 20528 let _snapToLines = true; 20529 let _line = 'auto'; 20530 let _lineAlign = 'start'; 20531 let _position = 50; 20532 let _positionAlign = 'middle'; 20533 let _size = 50; 20534 let _align = 'middle'; 20535 Object.defineProperty(cue, 'id', extend({}, baseObj, { 20536 get: function () { 20537 return _id; 20538 }, 20539 set: function (value) { 20540 _id = '' + value; 20541 } 20542 })); 20543 Object.defineProperty(cue, 'pauseOnExit', extend({}, baseObj, { 20544 get: function () { 20545 return _pauseOnExit; 20546 }, 20547 set: function (value) { 20548 _pauseOnExit = !!value; 20549 } 20550 })); 20551 Object.defineProperty(cue, 'startTime', extend({}, baseObj, { 20552 get: function () { 20553 return _startTime; 20554 }, 20555 set: function (value) { 20556 if (typeof value !== 'number') { 20557 throw new TypeError('Start time must be set to a number.'); 20558 } 20559 _startTime = value; 20560 this.hasBeenReset = true; 20561 } 20562 })); 20563 Object.defineProperty(cue, 'endTime', extend({}, baseObj, { 20564 get: function () { 20565 return _endTime; 20566 }, 20567 set: function (value) { 20568 if (typeof value !== 'number') { 20569 throw new TypeError('End time must be set to a number.'); 20570 } 20571 _endTime = value; 20572 this.hasBeenReset = true; 20573 } 20574 })); 20575 Object.defineProperty(cue, 'text', extend({}, baseObj, { 20576 get: function () { 20577 return _text; 20578 }, 20579 set: function (value) { 20580 _text = '' + value; 20581 this.hasBeenReset = true; 20582 } 20583 })); 20584 20585 // todo: implement VTTRegion polyfill? 20586 Object.defineProperty(cue, 'region', extend({}, baseObj, { 20587 get: function () { 20588 return _region; 20589 }, 20590 set: function (value) { 20591 _region = value; 20592 this.hasBeenReset = true; 20593 } 20594 })); 20595 Object.defineProperty(cue, 'vertical', extend({}, baseObj, { 20596 get: function () { 20597 return _vertical; 20598 }, 20599 set: function (value) { 20600 const setting = findDirectionSetting(value); 20601 // Have to check for false because the setting an be an empty string. 20602 if (setting === false) { 20603 throw new SyntaxError('An invalid or illegal string was specified.'); 20604 } 20605 _vertical = setting; 20606 this.hasBeenReset = true; 20607 } 20608 })); 20609 Object.defineProperty(cue, 'snapToLines', extend({}, baseObj, { 20610 get: function () { 20611 return _snapToLines; 20612 }, 20613 set: function (value) { 20614 _snapToLines = !!value; 20615 this.hasBeenReset = true; 20616 } 20617 })); 20618 Object.defineProperty(cue, 'line', extend({}, baseObj, { 20619 get: function () { 20620 return _line; 20621 }, 20622 set: function (value) { 20623 if (typeof value !== 'number' && value !== 'auto') { 20624 throw new SyntaxError('An invalid number or illegal string was specified.'); 20625 } 20626 _line = value; 20627 this.hasBeenReset = true; 20628 } 20629 })); 20630 Object.defineProperty(cue, 'lineAlign', extend({}, baseObj, { 20631 get: function () { 20632 return _lineAlign; 20633 }, 20634 set: function (value) { 20635 const setting = findAlignSetting(value); 20636 if (!setting) { 20637 throw new SyntaxError('An invalid or illegal string was specified.'); 20638 } 20639 _lineAlign = setting; 20640 this.hasBeenReset = true; 20641 } 20642 })); 20643 Object.defineProperty(cue, 'position', extend({}, baseObj, { 20644 get: function () { 20645 return _position; 20646 }, 20647 set: function (value) { 20648 if (value < 0 || value > 100) { 20649 throw new Error('Position must be between 0 and 100.'); 20650 } 20651 _position = value; 20652 this.hasBeenReset = true; 20653 } 20654 })); 20655 Object.defineProperty(cue, 'positionAlign', extend({}, baseObj, { 20656 get: function () { 20657 return _positionAlign; 20658 }, 20659 set: function (value) { 20660 const setting = findAlignSetting(value); 20661 if (!setting) { 20662 throw new SyntaxError('An invalid or illegal string was specified.'); 20663 } 20664 _positionAlign = setting; 20665 this.hasBeenReset = true; 20666 } 20667 })); 20668 Object.defineProperty(cue, 'size', extend({}, baseObj, { 20669 get: function () { 20670 return _size; 20671 }, 20672 set: function (value) { 20673 if (value < 0 || value > 100) { 20674 throw new Error('Size must be between 0 and 100.'); 20675 } 20676 _size = value; 20677 this.hasBeenReset = true; 20678 } 20679 })); 20680 Object.defineProperty(cue, 'align', extend({}, baseObj, { 20681 get: function () { 20682 return _align; 20683 }, 20684 set: function (value) { 20685 const setting = findAlignSetting(value); 20686 if (!setting) { 20687 throw new SyntaxError('An invalid or illegal string was specified.'); 20688 } 20689 _align = setting; 20690 this.hasBeenReset = true; 20691 } 20692 })); 20693 20694 /** 20695 * Other <track> spec defined properties 20696 */ 20697 20698 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 20699 cue.displayState = undefined; 20700 } 20701 20702 /** 20703 * VTTCue methods 20704 */ 20705 20706 VTTCue.prototype.getCueAsHTML = function () { 20707 // Assume WebVTT.convertCueToDOMTree is on the global. 20708 const WebVTT = self.WebVTT; 20709 return WebVTT.convertCueToDOMTree(self, this.text); 20710 }; 20711 // this is a polyfill hack 20712 return VTTCue; 20713})(); 20714 20715/* 20716 * Source: https://github.com/mozilla/vtt.js/blob/master/dist/vtt.js 20717 */ 20718 20719class StringDecoder { 20720
20720// eslint-disable-next-line @typescript-eslint/no-unused-vars
vendor: 14,009 bytes, lines 20720-21156
20720 20721 decode(data, options) { 20722 if (!data) { 20723 return ''; 20724 } 20725 if (typeof data !== 'string') { 20726 throw new Error('Error - expected string data.'); 20727 } 20728 return decodeURIComponent(encodeURIComponent(data)); 20729 } 20730} 20731 20732// Try to parse input as a time stamp. 20733function parseTimeStamp(input) { 20734 function computeSeconds(h, m, s, f) { 20735 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + parseFloat(f || 0); 20736 } 20737 const m = input.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/); 20738 if (!m) { 20739 return null; 20740 } 20741 if (parseFloat(m[2]) > 59) { 20742 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 20743 // First position is hours as it's over 59. 20744 return computeSeconds(m[2], m[3], 0, m[4]); 20745 } 20746 // Timestamp takes the form of [hours (optional)]:[minutes]:[seconds].[milliseconds] 20747 return computeSeconds(m[1], m[2], m[3], m[4]); 20748} 20749 20750// A settings object holds key/value pairs and will ignore anything but the first 20751// assignment to a specific key. 20752class Settings { 20753 constructor() { 20754 this.values = Object.create(null); 20755 } 20756 // Only accept the first assignment to any key. 20757 set(k, v) { 20758 if (!this.get(k) && v !== '') { 20759 this.values[k] = v; 20760 } 20761 } 20762 // Return the value for a key, or a default value. 20763 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 20764 // a number of possible default values as properties where 'defaultKey' is 20765 // the key of the property that will be chosen; otherwise it's assumed to be 20766 // a single value. 20767 get(k, dflt, defaultKey) { 20768 if (defaultKey) { 20769 return this.has(k) ? this.values[k] : dflt[defaultKey]; 20770 } 20771 return this.has(k) ? this.values[k] : dflt; 20772 } 20773 // Check whether we have a value for a key. 20774 has(k) { 20775 return k in this.values; 20776 } 20777 // Accept a setting if its one of the given alternatives. 20778 alt(k, v, a) { 20779 for (let n = 0; n < a.length; ++n) { 20780 if (v === a[n]) { 20781 this.set(k, v); 20782 break; 20783 } 20784 } 20785 } 20786 // Accept a setting if its a valid (signed) integer. 20787 integer(k, v) { 20788 if (/^-?\d+$/.test(v)) { 20789 // integer 20790 this.set(k, parseInt(v, 10)); 20791 } 20792 } 20793 // Accept a setting if its a valid percentage. 20794 percent(k, v) { 20795 if (/^([\d]{1,3})(\.[\d]*)?%$/.test(v)) { 20796 const percent = parseFloat(v); 20797 if (percent >= 0 && percent <= 100) { 20798 this.set(k, percent); 20799 return true; 20800 } 20801 } 20802 return false; 20803 } 20804} 20805 20806// Helper function to parse input into groups separated by 'groupDelim', and 20807// interpret each group as a key/value pair separated by 'keyValueDelim'. 20808function parseOptions(input, callback, keyValueDelim, groupDelim) { 20809 const groups = groupDelim ? input.split(groupDelim) : [input]; 20810 for (const i in groups) { 20811 if (typeof groups[i] !== 'string') { 20812 continue; 20813 } 20814 const kv = groups[i].split(keyValueDelim); 20815 if (kv.length !== 2) { 20816 continue; 20817 } 20818 const k = kv[0]; 20819 const v = kv[1]; 20820 callback(k, v); 20821 } 20822} 20823const defaults = new VTTCue(0, 0, ''); 20824// 'middle' was changed to 'center' in the spec: https://github.com/w3c/webvtt/pull/244 20825// Safari doesn't yet support this change, but FF and Chrome do. 20826const center = defaults.align === 'middle' ? 'middle' : 'center'; 20827function parseCue(input, cue, regionList) { 20828 // Remember the original input if we need to throw an error. 20829 const oInput = input; 20830 // 4.1 WebVTT timestamp 20831 function consumeTimeStamp() { 20832 const ts = parseTimeStamp(input); 20833 if (ts === null) { 20834 throw new Error('Malformed timestamp: ' + oInput); 20835 } 20836 20837 // Remove time stamp from input. 20838 input = input.replace(/^[^\sa-zA-Z-]+/, ''); 20839 return ts; 20840 } 20841 20842 // 4.4.2 WebVTT cue settings 20843 function consumeCueSettings(input, cue) { 20844 const settings = new Settings(); 20845 parseOptions(input, function (k, v) { 20846 let vals; 20847 switch (k) { 20848 case 'region': 20849 // Find the last region we parsed with the same region id. 20850 for (let i = regionList.length - 1; i >= 0; i--) { 20851 if (regionList[i].id === v) { 20852 settings.set(k, regionList[i].region); 20853 break; 20854 } 20855 } 20856 break; 20857 case 'vertical': 20858 settings.alt(k, v, ['rl', 'lr']); 20859 break; 20860 case 'line': 20861 vals = v.split(','); 20862 settings.integer(k, vals[0]); 20863 if (settings.percent(k, vals[0])) { 20864 settings.set('snapToLines', false); 20865 } 20866 settings.alt(k, vals[0], ['auto']); 20867 if (vals.length === 2) { 20868 settings.alt('lineAlign', vals[1], ['start', center, 'end']); 20869 } 20870 break; 20871 case 'position': 20872 vals = v.split(','); 20873 settings.percent(k, vals[0]); 20874 if (vals.length === 2) { 20875 settings.alt('positionAlign', vals[1], ['start', center, 'end', 'line-left', 'line-right', 'auto']); 20876 } 20877 break; 20878 case 'size': 20879 settings.percent(k, v); 20880 break; 20881 case 'align': 20882 settings.alt(k, v, ['start', center, 'end', 'left', 'right']); 20883 break; 20884 } 20885 }, /:/, /\s/); 20886 20887 // Apply default values for any missing fields. 20888 cue.region = settings.get('region', null); 20889 cue.vertical = settings.get('vertical', ''); 20890 let line = settings.get('line', 'auto'); 20891 if (line === 'auto' && defaults.line === -1) { 20892 // set numeric line number for Safari 20893 line = -1; 20894 } 20895 cue.line = line; 20896 cue.lineAlign = settings.get('lineAlign', 'start'); 20897 cue.snapToLines = settings.get('snapToLines', true); 20898 cue.size = settings.get('size', 100); 20899 cue.align = settings.get('align', center); 20900 let position = settings.get('position', 'auto'); 20901 if (position === 'auto' && defaults.position === 50) { 20902 // set numeric position for Safari 20903 position = cue.align === 'start' || cue.align === 'left' ? 0 : cue.align === 'end' || cue.align === 'right' ? 100 : 50; 20904 } 20905 cue.position = position; 20906 } 20907 function skipWhitespace() { 20908 input = input.replace(/^\s+/, ''); 20909 } 20910 20911 // 4.1 WebVTT cue timings. 20912 skipWhitespace(); 20913 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 20914 skipWhitespace(); 20915 if (input.slice(0, 3) !== '-->') { 20916 // (3) next characters must match '-->' 20917 throw new Error("Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 20918 } 20919 input = input.slice(3); 20920 skipWhitespace(); 20921 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 20922 20923 // 4.1 WebVTT cue settings list. 20924 skipWhitespace(); 20925 consumeCueSettings(input, cue); 20926} 20927function fixLineBreaks(input) { 20928 return input.replace(/<br(?: \/)?>/gi, '\n'); 20929} 20930class VTTParser { 20931 constructor() { 20932 this.state = 'INITIAL'; 20933 this.buffer = ''; 20934 this.decoder = new StringDecoder(); 20935 this.regionList = []; 20936 this.cue = null; 20937 this.oncue = void 0; 20938 this.onparsingerror = void 0; 20939 this.onflush = void 0; 20940 } 20941 parse(data) { 20942 const _this = this; 20943 20944 // If there is no data then we won't decode it, but will just try to parse 20945 // whatever is in buffer already. This may occur in circumstances, for 20946 // example when flush() is called. 20947 if (data) { 20948 // Try to decode the data that we received. 20949 _this.buffer += _this.decoder.decode(data, { 20950 stream: true 20951 }); 20952 } 20953 function collectNextLine() { 20954 let buffer = _this.buffer; 20955 let pos = 0; 20956 buffer = fixLineBreaks(buffer); 20957 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 20958 ++pos; 20959 } 20960 const line = buffer.slice(0, pos); 20961 // Advance the buffer early in case we fail below. 20962 if (buffer[pos] === '\r') { 20963 ++pos; 20964 } 20965 if (buffer[pos] === '\n') { 20966 ++pos; 20967 } 20968 _this.buffer = buffer.slice(pos); 20969 return line; 20970 } 20971 20972 // 3.2 WebVTT metadata header syntax 20973 function parseHeader(input) { 20974 parseOptions(input, function (k, v) { 20975 // switch (k) { 20976 // case 'region': 20977 // 3.3 WebVTT region metadata header syntax 20978 // console.log('parse region', v); 20979 // parseRegion(v); 20980 // break; 20981 // } 20982 }, /:/); 20983 } 20984 20985 // 5.1 WebVTT file parsing. 20986 try { 20987 let line = ''; 20988 if (_this.state === 'INITIAL') { 20989 // We can't start parsing until we have the first line. 20990 if (!/\r\n|\n/.test(_this.buffer)) { 20991 return this; 20992 } 20993 line = collectNextLine(); 20994 // strip of UTF-8 BOM if any 20995 // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 20996 const m = line.match(/^()?WEBVTT([ \t].*)?$/); 20997 if (!(m != null && m[0])) { 20998 throw new Error('Malformed WebVTT signature.'); 20999 } 21000 _this.state = 'HEADER'; 21001 } 21002 let alreadyCollectedLine = false; 21003 while (_this.buffer) { 21004 // We can't parse a line until we have the full line. 21005 if (!/\r\n|\n/.test(_this.buffer)) { 21006 return this; 21007 } 21008 if (!alreadyCollectedLine) { 21009 line = collectNextLine(); 21010 } else { 21011 alreadyCollectedLine = false; 21012 } 21013 switch (_this.state) { 21014 case 'HEADER': 21015 // 13-18 - Allow a header (metadata) under the WEBVTT line. 21016 if (/:/.test(line)) { 21017 parseHeader(line); 21018 } else if (!line) { 21019 // An empty line terminates the header and starts the body (cues). 21020 _this.state = 'ID'; 21021 } 21022 continue; 21023 case 'NOTE': 21024 // Ignore NOTE blocks. 21025 if (!line) { 21026 _this.state = 'ID'; 21027 } 21028 continue; 21029 case 'ID': 21030 // Check for the start of NOTE blocks. 21031 if (/^NOTE($|[ \t])/.test(line)) { 21032 _this.state = 'NOTE'; 21033 break; 21034 } 21035 // 19-29 - Allow any number of line terminators, then initialize new cue values. 21036 if (!line) { 21037 continue; 21038 } 21039 _this.cue = new VTTCue(0, 0, ''); 21040 _this.state = 'CUE'; 21041 // 30-39 - Check if self line contains an optional identifier or timing data. 21042 if (line.indexOf('-->') === -1) { 21043 _this.cue.id = line; 21044 continue; 21045 } 21046 // Process line as start of a cue. 21047 /* falls through */ 21048 case 'CUE': 21049 // 40 - Collect cue timings and settings. 21050 if (!_this.cue) { 21051 _this.state = 'BADCUE'; 21052 continue; 21053 } 21054 try { 21055 parseCue(line, _this.cue, _this.regionList); 21056 } catch (e) { 21057 // In case of an error ignore rest of the cue. 21058 _this.cue = null; 21059 _this.state = 'BADCUE'; 21060 continue; 21061 } 21062 _this.state = 'CUETEXT'; 21063 continue; 21064 case 'CUETEXT': 21065 { 21066 const hasSubstring = line.indexOf('-->') !== -1; 21067 // 34 - If we have an empty line then report the cue. 21068 // 35 - If we have the special substring '-->' then report the cue, 21069 // but do not collect the line as we need to process the current 21070 // one as a new cue. 21071 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 21072 // We are done parsing self cue. 21073 if (_this.oncue && _this.cue) { 21074 _this.oncue(_this.cue); 21075 } 21076 _this.cue = null; 21077 _this.state = 'ID'; 21078 continue; 21079 } 21080 if (_this.cue === null) { 21081 continue; 21082 } 21083 if (_this.cue.text) { 21084 _this.cue.text += '\n'; 21085 } 21086 _this.cue.text += line; 21087 } 21088 continue; 21089 case 'BADCUE': 21090 // 54-62 - Collect and discard the remaining cue. 21091 if (!line) { 21092 _this.state = 'ID'; 21093 } 21094 } 21095 } 21096 } catch (e) { 21097 // If we are currently parsing a cue, report what we have. 21098 if (_this.state === 'CUETEXT' && _this.cue && _this.oncue) { 21099 _this.oncue(_this.cue); 21100 } 21101 _this.cue = null; 21102 // Enter BADWEBVTT state if header was not parsed correctly otherwise 21103 // another exception occurred so enter BADCUE state. 21104 _this.state = _this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE'; 21105 } 21106 return this; 21107 } 21108 flush() { 21109 const _this = this; 21110 try { 21111 // Finish decoding the stream. 21112 // _this.buffer += _this.decoder.decode(); 21113 // Synthesize the end of the current cue or region. 21114 if (_this.cue || _this.state === 'HEADER') { 21115 _this.buffer += '\n\n'; 21116 _this.parse(); 21117 } 21118 // If we've flushed, parsed, and we're still on the INITIAL state then 21119 // that means we don't have enough of the stream to parse the first 21120 // line. 21121 if (_this.state === 'INITIAL' || _this.state === 'BADWEBVTT') { 21122 throw new Error('Malformed WebVTT signature.'); 21123 } 21124 } catch (e) { 21125 if (_this.onparsingerror) { 21126 _this.onparsingerror(e); 21127 } 21128 } 21129 if (_this.onflush) { 21130 _this.onflush(); 21131 } 21132 return this; 21133 } 21134} 21135 21136const LINEBREAKS = /\r\n|\n\r|\n|\r/g; 21137 21138// String.prototype.startsWith is not supported in IE11 21139const startsWith = function startsWith(inputString, searchString, position = 0) { 21140 return inputString.slice(position, position + searchString.length) === searchString; 21141}; 21142const cueString2millis = function cueString2millis(timeString) { 21143 let ts = parseInt(timeString.slice(-3)); 21144 const secs = parseInt(timeString.slice(-6, -4)); 21145 const mins = parseInt(timeString.slice(-9, -7)); 21146 const hours = timeString.length > 9 ? parseInt(timeString.substring(0, timeString.indexOf(':'))) : 0; 21147 if (!isFiniteNumber(ts) || !isFiniteNumber(secs) || !isFiniteNumber(mins) || !isFiniteNumber(hours)) { 21148 throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`); 21149 } 21150 ts += 1000 * secs; 21151 ts += 60 * 1000 * mins; 21152 ts += 60 * 60 * 1000 * hours; 21153 return ts; 21154}; 21155 21156//
vendor: 11,662 bytes, lines 21156-21496
21156From https://github.com/darkskyapp/string-hash 21157const hash = function hash(text) { 21158 let _hash = 5381; 21159 let i = text.length; 21160 while (i) { 21161 _hash = _hash * 33 ^ text.charCodeAt(--i); 21162 } 21163 return (_hash >>> 0).toString(); 21164}; 21165 21166// Create a unique hash id for a cue based on start/end times and text. 21167// This helps timeline-controller to avoid showing repeated captions. 21168function generateCueId(startTime, endTime, text) { 21169 return hash(startTime.toString()) + hash(endTime.toString()) + hash(text); 21170} 21171const calculateOffset = function calculateOffset(vttCCs, cc, presentationTime) { 21172 let currCC = vttCCs[cc]; 21173 let prevCC = vttCCs[currCC.prevCC]; 21174 21175 // This is the first discontinuity or cues have been processed since the last discontinuity 21176 // Offset = current discontinuity time 21177 if (!prevCC || !prevCC.new && currCC.new) { 21178 vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start; 21179 currCC.new = false; 21180 return; 21181 } 21182 21183 // There have been discontinuities since cues were last parsed. 21184 // Offset = time elapsed 21185 while ((_prevCC = prevCC) != null && _prevCC.new) { 21186 var _prevCC; 21187 vttCCs.ccOffset += currCC.start - prevCC.start; 21188 currCC.new = false; 21189 currCC = prevCC; 21190 prevCC = vttCCs[currCC.prevCC]; 21191 } 21192 vttCCs.presentationOffset = presentationTime; 21193}; 21194function parseWebVTT(vttByteArray, initPTS, vttCCs, cc, timeOffset, callBack, errorCallBack) { 21195 const parser = new VTTParser(); 21196 // Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character. 21197 // Uint8Array.prototype.reduce is not implemented in IE11 21198 const vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray)).trim().replace(LINEBREAKS, '\n').split('\n'); 21199 const cues = []; 21200 const init90kHz = initPTS ? toMpegTsClockFromTimescale(initPTS.baseTime, initPTS.timescale) : 0; 21201 let cueTime = '00:00.000'; 21202 let timestampMapMPEGTS = 0; 21203 let timestampMapLOCAL = 0; 21204 let parsingError; 21205 let inHeader = true; 21206 parser.oncue = function (cue) { 21207 // Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline. 21208 const currCC = vttCCs[cc]; 21209 let cueOffset = vttCCs.ccOffset; 21210 21211 // Calculate subtitle PTS offset 21212 const webVttMpegTsMapOffset = (timestampMapMPEGTS - init90kHz) / 90000; 21213 21214 // Update offsets for new discontinuities 21215 if (currCC != null && currCC.new) { 21216 if (timestampMapLOCAL !== undefined) { 21217 // When local time is provided, offset = discontinuity start time - local time 21218 cueOffset = vttCCs.ccOffset = currCC.start; 21219 } else { 21220 calculateOffset(vttCCs, cc, webVttMpegTsMapOffset); 21221 } 21222 } 21223 if (webVttMpegTsMapOffset) { 21224 if (!initPTS) { 21225 parsingError = new Error('Missing initPTS for VTT MPEGTS'); 21226 return; 21227 } 21228 // If we have MPEGTS, offset = presentation time + discontinuity offset 21229 cueOffset = webVttMpegTsMapOffset - vttCCs.presentationOffset; 21230 } 21231 const duration = cue.endTime - cue.startTime; 21232 const startTime = normalizePts((cue.startTime + cueOffset - timestampMapLOCAL) * 90000, timeOffset * 90000) / 90000; 21233 cue.startTime = Math.max(startTime, 0); 21234 cue.endTime = Math.max(startTime + duration, 0); 21235 21236 //trim trailing webvtt block whitespaces 21237 const text = cue.text.trim(); 21238 21239 // Fix encoding of special characters 21240 cue.text = decodeURIComponent(encodeURIComponent(text)); 21241 21242 // If the cue was not assigned an id from the VTT file (line above the content), create one. 21243 if (!cue.id) { 21244 cue.id = generateCueId(cue.startTime, cue.endTime, text); 21245 } 21246 if (cue.endTime > 0) { 21247 cues.push(cue); 21248 } 21249 }; 21250 parser.onparsingerror = function (error) { 21251 parsingError = error; 21252 }; 21253 parser.onflush = function () { 21254 if (parsingError) { 21255 errorCallBack(parsingError); 21256 return; 21257 } 21258 callBack(cues); 21259 }; 21260 21261 // Go through contents line by line. 21262 vttLines.forEach(line => { 21263 if (inHeader) { 21264 // Look for X-TIMESTAMP-MAP in header. 21265 if (startsWith(line, 'X-TIMESTAMP-MAP=')) { 21266 // Once found, no more are allowed anyway, so stop searching. 21267 inHeader = false; 21268 // Extract LOCAL and MPEGTS. 21269 line.slice(16).split(',').forEach(timestamp => { 21270 if (startsWith(timestamp, 'LOCAL:')) { 21271 cueTime = timestamp.slice(6); 21272 } else if (startsWith(timestamp, 'MPEGTS:')) { 21273 timestampMapMPEGTS = parseInt(timestamp.slice(7)); 21274 } 21275 }); 21276 try { 21277 // Convert cue time to seconds 21278 timestampMapLOCAL = cueString2millis(cueTime) / 1000; 21279 } catch (error) { 21280 parsingError = error; 21281 } 21282 // Return without parsing X-TIMESTAMP-MAP line. 21283 return; 21284 } else if (line === '') { 21285 inHeader = false; 21286 } 21287 } 21288 // Parse line by default. 21289 parser.parse(line + '\n'); 21290 }); 21291 parser.flush(); 21292} 21293 21294const IMSC1_CODEC = 'stpp.ttml.im1t'; 21295 21296// Time format: h:m:s:frames(.subframes) 21297const HMSF_REGEX = /^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/; 21298 21299// Time format: hours, minutes, seconds, milliseconds, frames, ticks 21300const TIME_UNIT_REGEX = /^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/; 21301const textAlignToLineAlign = { 21302 left: 'start', 21303 center: 'center', 21304 right: 'end', 21305 start: 'start', 21306 end: 'end' 21307}; 21308function parseIMSC1(payload, initPTS, callBack, errorCallBack) { 21309 const results = findBox(new Uint8Array(payload), ['mdat']); 21310 if (results.length === 0) { 21311 errorCallBack(new Error('Could not parse IMSC1 mdat')); 21312 return; 21313 } 21314 const ttmlList = results.map(mdat => utf8ArrayToStr(mdat)); 21315 const syncTime = toTimescaleFromScale(initPTS.baseTime, 1, initPTS.timescale); 21316 try { 21317 ttmlList.forEach(ttml => callBack(parseTTML(ttml, syncTime))); 21318 } catch (error) { 21319 errorCallBack(error); 21320 } 21321} 21322function parseTTML(ttml, syncTime) { 21323 const parser = new DOMParser(); 21324 const xmlDoc = parser.parseFromString(ttml, 'text/xml'); 21325 const tt = xmlDoc.getElementsByTagName('tt')[0]; 21326 if (!tt) { 21327 throw new Error('Invalid ttml'); 21328 } 21329 const defaultRateInfo = { 21330 frameRate: 30, 21331 subFrameRate: 1, 21332 frameRateMultiplier: 0, 21333 tickRate: 0 21334 }; 21335 const rateInfo = Object.keys(defaultRateInfo).reduce((result, key) => { 21336 result[key] = tt.getAttribute(`ttp:${key}`) || defaultRateInfo[key]; 21337 return result; 21338 }, {}); 21339 const trim = tt.getAttribute('xml:space') !== 'preserve'; 21340 const styleElements = collectionToDictionary(getElementCollection(tt, 'styling', 'style')); 21341 const regionElements = collectionToDictionary(getElementCollection(tt, 'layout', 'region')); 21342 const cueElements = getElementCollection(tt, 'body', '[begin]'); 21343 return [].map.call(cueElements, cueElement => { 21344 const cueText = getTextContent(cueElement, trim); 21345 if (!cueText || !cueElement.hasAttribute('begin')) { 21346 return null; 21347 } 21348 const startTime = parseTtmlTime(cueElement.getAttribute('begin'), rateInfo); 21349 const duration = parseTtmlTime(cueElement.getAttribute('dur'), rateInfo); 21350 let endTime = parseTtmlTime(cueElement.getAttribute('end'), rateInfo); 21351 if (startTime === null) { 21352 throw timestampParsingError(cueElement); 21353 } 21354 if (endTime === null) { 21355 if (duration === null) { 21356 throw timestampParsingError(cueElement); 21357 } 21358 endTime = startTime + duration; 21359 } 21360 const cue = new VTTCue(startTime - syncTime, endTime - syncTime, cueText); 21361 cue.id = generateCueId(cue.startTime, cue.endTime, cue.text); 21362 const region = regionElements[cueElement.getAttribute('region')]; 21363 const style = styleElements[cueElement.getAttribute('style')]; 21364 21365 // Apply styles to cue 21366 const styles = getTtmlStyles(region, style, styleElements); 21367 const { 21368 textAlign 21369 } = styles; 21370 if (textAlign) { 21371 // cue.positionAlign not settable in FF~2016 21372 const lineAlign = textAlignToLineAlign[textAlign]; 21373 if (lineAlign) { 21374 cue.lineAlign = lineAlign; 21375 } 21376 cue.align = textAlign; 21377 } 21378 _extends(cue, styles); 21379 return cue; 21380 }).filter(cue => cue !== null); 21381} 21382function getElementCollection(fromElement, parentName, childName) { 21383 const parent = fromElement.getElementsByTagName(parentName)[0]; 21384 if (parent) { 21385 return [].slice.call(parent.querySelectorAll(childName)); 21386 } 21387 return []; 21388} 21389function collectionToDictionary(elementsWithId) { 21390 return elementsWithId.reduce((dict, element) => { 21391 const id = element.getAttribute('xml:id'); 21392 if (id) { 21393 dict[id] = element; 21394 } 21395 return dict; 21396 }, {}); 21397} 21398function getTextContent(element, trim) { 21399 return [].slice.call(element.childNodes).reduce((str, node, i) => { 21400 var _node$childNodes; 21401 if (node.nodeName === 'br' && i) { 21402 return str + '\n'; 21403 } 21404 if ((_node$childNodes = node.childNodes) != null && _node$childNodes.length) { 21405 return getTextContent(node, trim); 21406 } else if (trim) { 21407 return str + node.textContent.trim().replace(/\s+/g, ' '); 21408 } 21409 return str + node.textContent; 21410 }, ''); 21411} 21412function getTtmlStyles(region, style, styleElements) { 21413 const ttsNs = 'http://www.w3.org/ns/ttml#styling'; 21414 let regionStyle = null; 21415 const styleAttributes = ['displayAlign', 'textAlign', 'color', 'backgroundColor', 'fontSize', 'fontFamily' 21416 // 'fontWeight', 21417 // 'lineHeight', 21418 // 'wrapOption', 21419 // 'fontStyle', 21420 // 'direction', 21421 // 'writingMode' 21422 ]; 21423 const regionStyleName = region != null && region.hasAttribute('style') ? region.getAttribute('style') : null; 21424 if (regionStyleName && styleElements.hasOwnProperty(regionStyleName)) { 21425 regionStyle = styleElements[regionStyleName]; 21426 } 21427 return styleAttributes.reduce((styles, name) => { 21428 const value = getAttributeNS(style, ttsNs, name) || getAttributeNS(region, ttsNs, name) || getAttributeNS(regionStyle, ttsNs, name); 21429 if (value) { 21430 styles[name] = value; 21431 } 21432 return styles; 21433 }, {}); 21434} 21435function getAttributeNS(element, ns, name) { 21436 if (!element) { 21437 return null; 21438 } 21439 return element.hasAttributeNS(ns, name) ? element.getAttributeNS(ns, name) : null; 21440} 21441function timestampParsingError(node) { 21442 return new Error(`Could not parse ttml timestamp ${node}`); 21443} 21444function parseTtmlTime(timeAttributeValue, rateInfo) { 21445 if (!timeAttributeValue) { 21446 return null; 21447 } 21448 let seconds = parseTimeStamp(timeAttributeValue); 21449 if (seconds === null) { 21450 if (HMSF_REGEX.test(timeAttributeValue)) { 21451 seconds = parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo); 21452 } else if (TIME_UNIT_REGEX.test(timeAttributeValue)) { 21453 seconds = parseTimeUnits(timeAttributeValue, rateInfo); 21454 } 21455 } 21456 return seconds; 21457} 21458function parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo) { 21459 const m = HMSF_REGEX.exec(timeAttributeValue); 21460 const frames = (m[4] | 0) + (m[5] | 0) / rateInfo.subFrameRate; 21461 return (m[1] | 0) * 3600 + (m[2] | 0) * 60 + (m[3] | 0) + frames / rateInfo.frameRate; 21462} 21463function parseTimeUnits(timeAttributeValue, rateInfo) { 21464 const m = TIME_UNIT_REGEX.exec(timeAttributeValue); 21465 const value = Number(m[1]); 21466 const unit = m[2]; 21467 switch (unit) { 21468 case 'h': 21469 return value * 3600; 21470 case 'm': 21471 return value * 60; 21472 case 'ms': 21473 return value * 1000; 21474 case 'f': 21475 return value / rateInfo.frameRate; 21476 case 't': 21477 return value / rateInfo.tickRate; 21478 } 21479 return value; 21480} 21481 21482class TimelineController { 21483 constructor(hls) { 21484 this.hls = void 0; 21485 this.media = null; 21486 this.config = void 0; 21487 this.enabled = true; 21488 this.Cues = void 0; 21489 this.textTracks = []; 21490 this.tracks = []; 21491 this.initPTS = []; 21492 this.unparsedVttFrags = []; 21493 this.captionsTracks = {}; 21494 this.nonNativeCaptionsTracks = {}; 21495 this.cea608Parser1 = void 0; 21496 this.cea608Parser2 = void 0;
21497 this.lastCc = -1; 21498 // Last video (CEA-608) fragment CC 21499 this.lastSn = -1; 21500 // Last video (CEA-608) fragment MSN 21501 this.lastPartIndex = -1; 21502 // Last video (CEA-608) fragment Part Index 21503 this.prevCC = -1; 21504 // Last subtitle fragment CC 21505 this.vttCCs = newVTTCCs(); 21506 this.captionsProperties = void 0; 21507 this.hls = hls; 21508 this.config = hls.config; 21509 this.Cues = hls.config.cueHandler; 21510 this.captionsProperties = { 21511 textTrack1: { 21512 label: this.config.captionsTextTrack1Label, 21513 languageCode: this.config.captionsTextTrack1LanguageCode 21514 }, 21515 textTrack2: { 21516 label: this.config.captionsTextTrack2Label, 21517 languageCode: this.config.captionsTextTrack2LanguageCode 21518 }, 21519 textTrack3: { 21520 label: this.config.captionsTextTrack3Label, 21521 languageCode: this.config.captionsTextTrack3LanguageCode 21522 }, 21523 textTrack4: { 21524 label: this.config.captionsTextTrack4Label, 21525 languageCode: this.config.captionsTextTrack4LanguageCode 21526 } 21527 }; 21528 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 21529 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 21530 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 21531 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 21532 hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 21533 hls.on(Events.FRAG_LOADING, this.onFragLoading, this); 21534 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 21535 hls.on(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); 21536 hls.on(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); 21537 hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 21538 hls.on(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); 21539 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 21540 } 21541 destroy() { 21542 const { 21543 hls 21544 } = this; 21545 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 21546 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 21547 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 21548 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 21549 hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 21550 hls.off(Events.FRAG_LOADING, this.onFragLoading, this); 21551 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 21552 hls.off(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); 21553 hls.off(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); 21554 hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 21555 hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); 21556 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 21557 // @ts-ignore 21558 this.hls = this.config = null; 21559 this.cea608Parser1 = this.cea608Parser2 = undefined; 21560 } 21561 initCea608Parsers() { 21562 if (this.config.enableCEA708Captions && (!this.cea608Parser1 || !this.cea608Parser2)) { 21563 const channel1 = new OutputFilter(this, 'textTrack1'); 21564 const channel2 = new OutputFilter(this, 'textTrack2'); 21565 const channel3 = new OutputFilter(this, 'textTrack3'); 21566 const channel4 = new OutputFilter(this, 'textTrack4'); 21567 this.cea608Parser1 = new Cea608Parser(1, channel1, channel2); 21568 this.cea608Parser2 = new Cea608Parser(3, channel3, channel4); 21569 } 21570 } 21571 addCues(trackName, startTime, endTime, screen, cueRanges) { 21572 // skip cues which overlap more than 50% with previously parsed time ranges 21573 let merged = false; 21574 for (let i = cueRanges.length; i--;) { 21575 const cueRange = cueRanges[i]; 21576 const overlap = intersection(cueRange[0], cueRange[1], startTime, endTime); 21577 if (overlap >= 0) { 21578 cueRange[0] = Math.min(cueRange[0], startTime); 21579 cueRange[1] = Math.max(cueRange[1], endTime); 21580 merged = true; 21581 if (overlap / (endTime - startTime) > 0.5) { 21582 return; 21583 } 21584 } 21585 } 21586 if (!merged) { 21587 cueRanges.push([startTime, endTime]); 21588 } 21589 if (this.config.renderTextTracksNatively) { 21590 const track = this.captionsTracks[trackName]; 21591 this.Cues.newCue(track, startTime, endTime, screen); 21592 } else { 21593 const cues = this.Cues.newCue(null, startTime, endTime, screen); 21594 this.hls.trigger(Events.CUES_PARSED, { 21595 type: 'captions', 21596 cues, 21597 track: trackName 21598 }); 21599 } 21600 } 21601 21602 // Triggered when an initial PTS is found; used for synchronisation of WebVTT. 21603 onInitPtsFound(event, { 21604 frag, 21605 id, 21606 initPTS, 21607 timescale 21608 }) { 21609 const { 21610 unparsedVttFrags 21611 } = this; 21612 if (id === 'main') { 21613 this.initPTS[frag.cc] = { 21614 baseTime: initPTS, 21615 timescale 21616 }; 21617 } 21618 21619 // Due to asynchronous processing, initial PTS may arrive later than the first VTT fragments are loaded. 21620 // Parse any unparsed fragments upon receiving the initial PTS. 21621 if (unparsedVttFrags.length) { 21622 this.unparsedVttFrags = [];
vendor: 12,390 bytes, lines 21623-21996
21623 unparsedVttFrags.forEach(frag => { 21624 this.onFragLoaded(Events.FRAG_LOADED, frag); 21625 }); 21626 } 21627 } 21628 getExistingTrack(label, language) { 21629 const { 21630 media 21631 } = this; 21632 if (media) { 21633 for (let i = 0; i < media.textTracks.length; i++) { 21634 const textTrack = media.textTracks[i]; 21635 if (canReuseVttTextTrack(textTrack, { 21636 name: label, 21637 lang: language})) { 21638 return textTrack; 21639 } 21640 } 21641 } 21642 return null; 21643 } 21644 createCaptionsTrack(trackName) { 21645 if (this.config.renderTextTracksNatively) { 21646 this.createNativeTrack(trackName); 21647 } else { 21648 this.createNonNativeTrack(trackName); 21649 } 21650 } 21651 createNativeTrack(trackName) { 21652 if (this.captionsTracks[trackName]) { 21653 return; 21654 } 21655 const { 21656 captionsProperties, 21657 captionsTracks, 21658 media 21659 } = this; 21660 const { 21661 label, 21662 languageCode 21663 } = captionsProperties[trackName]; 21664 // Enable reuse of existing text track. 21665 const existingTrack = this.getExistingTrack(label, languageCode); 21666 if (!existingTrack) { 21667 const textTrack = this.createTextTrack('captions', label, languageCode); 21668 if (textTrack) { 21669 // Set a special property on the track so we know it's managed by Hls.js 21670 textTrack[trackName] = true; 21671 captionsTracks[trackName] = textTrack; 21672 } 21673 } else { 21674 captionsTracks[trackName] = existingTrack; 21675 clearCurrentCues(captionsTracks[trackName]); 21676 sendAddTrackEvent(captionsTracks[trackName], media); 21677 } 21678 } 21679 createNonNativeTrack(trackName) { 21680 if (this.nonNativeCaptionsTracks[trackName]) { 21681 return; 21682 } 21683 // Create a list of a single track for the provider to consume 21684 const trackProperties = this.captionsProperties[trackName]; 21685 if (!trackProperties) { 21686 return; 21687 } 21688 const label = trackProperties.label; 21689 const track = { 21690 _id: trackName, 21691 label, 21692 kind: 'captions', 21693 default: trackProperties.media ? !!trackProperties.media.default : false, 21694 closedCaptions: trackProperties.media 21695 }; 21696 this.nonNativeCaptionsTracks[trackName] = track; 21697 this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { 21698 tracks: [track] 21699 }); 21700 } 21701 createTextTrack(kind, label, lang) { 21702 const media = this.media; 21703 if (!media) { 21704 return; 21705 } 21706 return media.addTextTrack(kind, label, lang); 21707 } 21708 onMediaAttaching(event, data) { 21709 this.media = data.media; 21710 this._cleanTracks(); 21711 } 21712 onMediaDetaching() { 21713 const { 21714 captionsTracks 21715 } = this; 21716 Object.keys(captionsTracks).forEach(trackName => { 21717 clearCurrentCues(captionsTracks[trackName]); 21718 delete captionsTracks[trackName]; 21719 }); 21720 this.nonNativeCaptionsTracks = {}; 21721 } 21722 onManifestLoading() { 21723 // Detect discontinuity in video fragment (CEA-608) parsing 21724 this.lastCc = -1; 21725 this.lastSn = -1; 21726 this.lastPartIndex = -1; 21727 // Detect discontinuity in subtitle manifests 21728 this.prevCC = -1; 21729 this.vttCCs = newVTTCCs(); 21730 // Reset tracks 21731 this._cleanTracks(); 21732 this.tracks = []; 21733 this.captionsTracks = {}; 21734 this.nonNativeCaptionsTracks = {}; 21735 this.textTracks = []; 21736 this.unparsedVttFrags = []; 21737 this.initPTS = []; 21738 if (this.cea608Parser1 && this.cea608Parser2) { 21739 this.cea608Parser1.reset(); 21740 this.cea608Parser2.reset(); 21741 } 21742 } 21743 _cleanTracks() { 21744 // clear outdated subtitles 21745 const { 21746 media 21747 } = this; 21748 if (!media) { 21749 return; 21750 } 21751 const textTracks = media.textTracks; 21752 if (textTracks) { 21753 for (let i = 0; i < textTracks.length; i++) { 21754 clearCurrentCues(textTracks[i]); 21755 } 21756 } 21757 } 21758 onSubtitleTracksUpdated(event, data) { 21759 const tracks = data.subtitleTracks || []; 21760 const hasIMSC1 = tracks.some(track => track.textCodec === IMSC1_CODEC); 21761 if (this.config.enableWebVTT || hasIMSC1 && this.config.enableIMSC1) { 21762 const listIsIdentical = subtitleOptionsIdentical(this.tracks, tracks); 21763 if (listIsIdentical) { 21764 this.tracks = tracks; 21765 return; 21766 } 21767 this.textTracks = []; 21768 this.tracks = tracks; 21769 if (this.config.renderTextTracksNatively) { 21770 const media = this.media; 21771 const inUseTracks = media ? filterSubtitleTracks(media.textTracks) : null; 21772 this.tracks.forEach((track, index) => { 21773 // Reuse tracks with the same label and lang, but do not reuse 608/708 tracks 21774 let textTrack; 21775 if (inUseTracks) { 21776 let inUseTrack = null; 21777 for (let i = 0; i < inUseTracks.length; i++) { 21778 if (inUseTracks[i] && canReuseVttTextTrack(inUseTracks[i], track)) { 21779 inUseTrack = inUseTracks[i]; 21780 inUseTracks[i] = null; 21781 break; 21782 } 21783 } 21784 if (inUseTrack) { 21785 textTrack = inUseTrack; 21786 } 21787 } 21788 if (textTrack) { 21789 clearCurrentCues(textTrack); 21790 } else { 21791 const textTrackKind = captionsOrSubtitlesFromCharacteristics(track); 21792 textTrack = this.createTextTrack(textTrackKind, track.name, track.lang); 21793 if (textTrack) { 21794 textTrack.mode = 'disabled'; 21795 } 21796 } 21797 if (textTrack) { 21798 this.textTracks.push(textTrack); 21799 } 21800 }); 21801 // Warn when video element has captions or subtitle TextTracks carried over from another source 21802 if (inUseTracks != null && inUseTracks.length) { 21803 const unusedTextTracks = inUseTracks.filter(t => t !== null).map(t => t.label); 21804 if (unusedTextTracks.length) { 21805 logger.warn(`Media element contains unused subtitle tracks: ${unusedTextTracks.join(', ')}. Replace media element for each source to clear TextTracks and captions menu.`); 21806 } 21807 } 21808 } else if (this.tracks.length) { 21809 // Create a list of tracks for the provider to consume 21810 const tracksList = this.tracks.map(track => { 21811 return { 21812 label: track.name, 21813 kind: track.type.toLowerCase(), 21814 default: track.default, 21815 subtitleTrack: track 21816 }; 21817 }); 21818 this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { 21819 tracks: tracksList 21820 }); 21821 } 21822 } 21823 } 21824 onManifestLoaded(event, data) { 21825 if (this.config.enableCEA708Captions && data.captions) { 21826 data.captions.forEach(captionsTrack => { 21827 const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(captionsTrack.instreamId); 21828 if (!instreamIdMatch) { 21829 return; 21830 } 21831 const trackName = `textTrack${instreamIdMatch[1]}`; 21832 const trackProperties = this.captionsProperties[trackName]; 21833 if (!trackProperties) { 21834 return; 21835 } 21836 trackProperties.label = captionsTrack.name; 21837 if (captionsTrack.lang) { 21838 // optional attribute 21839 trackProperties.languageCode = captionsTrack.lang; 21840 } 21841 trackProperties.media = captionsTrack; 21842 }); 21843 } 21844 } 21845 closedCaptionsForLevel(frag) { 21846 const level = this.hls.levels[frag.level]; 21847 return level == null ? void 0 : level.attrs['CLOSED-CAPTIONS']; 21848 } 21849 onFragLoading(event, data) { 21850 // if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack 21851 if (this.enabled && data.frag.type === PlaylistLevelType.MAIN) { 21852 var _data$part$index, _data$part; 21853 const { 21854 cea608Parser1, 21855 cea608Parser2, 21856 lastSn 21857 } = this; 21858 const { 21859 cc, 21860 sn 21861 } = data.frag; 21862 const partIndex = (_data$part$index = (_data$part = data.part) == null ? void 0 : _data$part.index) != null ? _data$part$index : -1; 21863 if (cea608Parser1 && cea608Parser2) { 21864 if (sn !== lastSn + 1 || sn === lastSn && partIndex !== this.lastPartIndex + 1 || cc !== this.lastCc) { 21865 cea608Parser1.reset(); 21866 cea608Parser2.reset(); 21867 } 21868 } 21869 this.lastCc = cc; 21870 this.lastSn = sn; 21871 this.lastPartIndex = partIndex; 21872 } 21873 } 21874 onFragLoaded(event, data) { 21875 const { 21876 frag, 21877 payload 21878 } = data; 21879 if (frag.type === PlaylistLevelType.SUBTITLE) { 21880 // If fragment is subtitle type, parse as WebVTT. 21881 if (payload.byteLength) { 21882 const decryptData = frag.decryptdata; 21883 // fragment after decryption has a stats object 21884 const decrypted = ('stats' in data); 21885 // If the subtitles are not encrypted, parse VTTs now. Otherwise, we need to wait. 21886 if (decryptData == null || !decryptData.encrypted || decrypted) { 21887 const trackPlaylistMedia = this.tracks[frag.level]; 21888 const vttCCs = this.vttCCs; 21889 if (!vttCCs[frag.cc]) { 21890 vttCCs[frag.cc] = { 21891 start: frag.start, 21892 prevCC: this.prevCC, 21893 new: true 21894 }; 21895 this.prevCC = frag.cc; 21896 } 21897 if (trackPlaylistMedia && trackPlaylistMedia.textCodec === IMSC1_CODEC) { 21898 this._parseIMSC1(frag, payload); 21899 } else { 21900 this._parseVTTs(data); 21901 } 21902 } 21903 } else { 21904 // In case there is no payload, finish unsuccessfully. 21905 this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 21906 success: false, 21907 frag, 21908 error: new Error('Empty subtitle payload') 21909 }); 21910 } 21911 } 21912 } 21913 _parseIMSC1(frag, payload) { 21914 const hls = this.hls; 21915 parseIMSC1(payload, this.initPTS[frag.cc], cues => { 21916 this._appendCues(cues, frag.level); 21917 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 21918 success: true, 21919 frag: frag 21920 }); 21921 }, error => { 21922 logger.log(`Failed to parse IMSC1: ${error}`); 21923 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 21924 success: false, 21925 frag: frag, 21926 error 21927 }); 21928 }); 21929 } 21930 _parseVTTs(data) { 21931 var _frag$initSegment; 21932 const { 21933 frag, 21934 payload 21935 } = data; 21936 // We need an initial synchronisation PTS. Store fragments as long as none has arrived 21937 const { 21938 initPTS, 21939 unparsedVttFrags 21940 } = this; 21941 const maxAvCC = initPTS.length - 1; 21942 if (!initPTS[frag.cc] && maxAvCC === -1) { 21943 unparsedVttFrags.push(data); 21944 return; 21945 } 21946 const hls = this.hls; 21947 // Parse the WebVTT file contents. 21948 const payloadWebVTT = (_frag$initSegment = frag.initSegment) != null && _frag$initSegment.data ? appendUint8Array(frag.initSegment.data, new Uint8Array(payload)) : payload; 21949 parseWebVTT(payloadWebVTT, this.initPTS[frag.cc], this.vttCCs, frag.cc, frag.start, cues => { 21950 this._appendCues(cues, frag.level); 21951 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 21952 success: true, 21953 frag: frag 21954 }); 21955 }, error => { 21956 const missingInitPTS = error.message === 'Missing initPTS for VTT MPEGTS'; 21957 if (missingInitPTS) { 21958 unparsedVttFrags.push(data); 21959 } else { 21960 this._fallbackToIMSC1(frag, payload); 21961 } 21962 // Something went wrong while parsing. Trigger event with success false. 21963 logger.log(`Failed to parse VTT cue: ${error}`); 21964 if (missingInitPTS && maxAvCC > frag.cc) { 21965 return; 21966 } 21967 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 21968 success: false, 21969 frag: frag, 21970 error 21971 }); 21972 }); 21973 } 21974 _fallbackToIMSC1(frag, payload) { 21975 // If textCodec is unknown, try parsing as IMSC1. Set textCodec based on the result 21976 const trackPlaylistMedia = this.tracks[frag.level]; 21977 if (!trackPlaylistMedia.textCodec) { 21978 parseIMSC1(payload, this.initPTS[frag.cc], () => { 21979 trackPlaylistMedia.textCodec = IMSC1_CODEC; 21980 this._parseIMSC1(frag, payload); 21981 }, () => { 21982 trackPlaylistMedia.textCodec = 'wvtt'; 21983 }); 21984 } 21985 } 21986 _appendCues(cues, fragLevel) { 21987 const hls = this.hls; 21988 if (this.config.renderTextTracksNatively) { 21989 const textTrack = this.textTracks[fragLevel]; 21990 // WebVTTParser.parse is an async method and if the currently selected text track mode is set to "disabled" 21991 // before parsing is done then don't try to access currentTrack.cues.getCueById as cues will be null 21992 // and trying to access getCueById method of cues will throw an exception 21993 // Because we check if the mode is disabled, we can force check `cues` below. They can't be null. 21994 if (!textTrack || textTrack.mode === 'disabled') { 21995 return; 21996 }
21997 cues.forEach(cue => addCueToTrack(textTrack, cue)); 21998 } else { 21999 const currentTrack = this.tracks[fragLevel]; 22000 if (!currentTrack) { 22001 return; 22002 } 22003 const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel; 22004 hls.trigger(Events.CUES_PARSED, { 22005 type: 'subtitles', 22006 cues, 22007 track 22008 }); 22009 } 22010 } 22011 onFragDecrypted(event, data) { 22012 const { 22013 frag 22014 } = data; 22015 if (frag.type === PlaylistLevelType.SUBTITLE) { 22016 this.onFragLoaded(Events.FRAG_LOADED, data); 22017 } 22018 } 22019 onSubtitleTracksCleared() { 22020 this.tracks = []; 22021 this.captionsTracks = {}; 22022 } 22023 onFragParsingUserdata(event, data) { 22024 this.initCea608Parsers(); 22025 const { 22026 cea608Parser1, 22027 cea608Parser2 22028 } = this; 22029 if (!this.enabled || !cea608Parser1 || !cea608Parser2) { 22030 return; 22031 } 22032 const { 22033 frag, 22034 samples 22035 } = data; 22036 if (frag.type === PlaylistLevelType.MAIN && this.closedCaptionsForLevel(frag) === 'NONE') { 22037 return; 22038 } 22039 // If the event contains captions (found in the bytes property), push all bytes into the parser immediately 22040 // It will create the proper timestamps based on the PTS value 22041 for (let i = 0; i < samples.length; i++) { 22042 const ccBytes = samples[i].bytes; 22043 if (ccBytes) { 22044 const ccdatas = this.extractCea608Data(ccBytes); 22045 cea608Parser1.addData(samples[i].pts, ccdatas[0]); 22046 cea608Parser2.addData(samples[i].pts, ccdatas[1]); 22047 } 22048 } 22049 } 22050 onBufferFlushing(event, { 22051 startOffset, 22052 endOffset, 22053 endOffsetSubtitles, 22054 type 22055 }) { 22056 const { 22057 media 22058 } = this; 22059 if (!media || media.currentTime < endOffset) { 22060 return; 22061 } 22062 // Clear 608 caption cues from the captions TextTracks when the video back buffer is flushed 22063 // Forward cues are never removed because we can loose streamed 608 content from recent fragments 22064 if (!type || type === 'video') { 22065 const { 22066 captionsTracks 22067 } = this;
vendor: 8,530 bytes, lines 22068-22317
22068 Object.keys(captionsTracks).forEach(trackName => removeCuesInRange(captionsTracks[trackName], startOffset, endOffset)); 22069 } 22070 if (this.config.renderTextTracksNatively) { 22071 // Clear VTT/IMSC1 subtitle cues from the subtitle TextTracks when the back buffer is flushed 22072 if (startOffset === 0 && endOffsetSubtitles !== undefined) { 22073 const { 22074 textTracks 22075 } = this; 22076 Object.keys(textTracks).forEach(trackName => removeCuesInRange(textTracks[trackName], startOffset, endOffsetSubtitles)); 22077 } 22078 } 22079 } 22080 extractCea608Data(byteArray) { 22081 const actualCCBytes = [[], []]; 22082 const count = byteArray[0] & 0x1f; 22083 let position = 2; 22084 for (let j = 0; j < count; j++) { 22085 const tmpByte = byteArray[position++]; 22086 const ccbyte1 = 0x7f & byteArray[position++]; 22087 const ccbyte2 = 0x7f & byteArray[position++]; 22088 if (ccbyte1 === 0 && ccbyte2 === 0) { 22089 continue; 22090 } 22091 const ccValid = (0x04 & tmpByte) !== 0; // Support all four channels 22092 if (ccValid) { 22093 const ccType = 0x03 & tmpByte; 22094 if (0x00 /* CEA608 field1*/ === ccType || 0x01 /* CEA608 field2*/ === ccType) { 22095 // Exclude CEA708 CC data. 22096 actualCCBytes[ccType].push(ccbyte1); 22097 actualCCBytes[ccType].push(ccbyte2); 22098 } 22099 } 22100 } 22101 return actualCCBytes; 22102 } 22103} 22104function captionsOrSubtitlesFromCharacteristics(track) { 22105 if (track.characteristics) { 22106 if (/transcribes-spoken-dialog/gi.test(track.characteristics) && /describes-music-and-sound/gi.test(track.characteristics)) { 22107 return 'captions'; 22108 } 22109 } 22110 return 'subtitles'; 22111} 22112function canReuseVttTextTrack(inUseTrack, manifestTrack) { 22113 return !!inUseTrack && inUseTrack.kind === captionsOrSubtitlesFromCharacteristics(manifestTrack) && subtitleTrackMatchesTextTrack(manifestTrack, inUseTrack); 22114} 22115function intersection(x1, x2, y1, y2) { 22116 return Math.min(x2, y2) - Math.max(x1, y1); 22117} 22118function newVTTCCs() { 22119 return { 22120 ccOffset: 0, 22121 presentationOffset: 0, 22122 0: { 22123 start: 0, 22124 prevCC: -1, 22125 new: true 22126 } 22127 }; 22128} 22129 22130class CapLevelController { 22131 constructor(hls) { 22132 this.hls = void 0; 22133 this.autoLevelCapping = void 0; 22134 this.firstLevel = void 0; 22135 this.media = void 0; 22136 this.restrictedLevels = void 0; 22137 this.timer = void 0; 22138 this.clientRect = void 0; 22139 this.streamController = void 0; 22140 this.hls = hls; 22141 this.autoLevelCapping = Number.POSITIVE_INFINITY; 22142 this.firstLevel = -1; 22143 this.media = null; 22144 this.restrictedLevels = []; 22145 this.timer = undefined; 22146 this.clientRect = null; 22147 this.registerListeners(); 22148 } 22149 setStreamController(streamController) { 22150 this.streamController = streamController; 22151 } 22152 destroy() { 22153 if (this.hls) { 22154 this.unregisterListener(); 22155 } 22156 if (this.timer) { 22157 this.stopCapping(); 22158 } 22159 this.media = null; 22160 this.clientRect = null; 22161 // @ts-ignore 22162 this.hls = this.streamController = null; 22163 } 22164 registerListeners() { 22165 const { 22166 hls 22167 } = this; 22168 hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); 22169 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 22170 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 22171 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 22172 hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); 22173 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 22174 } 22175 unregisterListener() { 22176 const { 22177 hls 22178 } = this; 22179 hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); 22180 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 22181 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 22182 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 22183 hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 22184 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 22185 } 22186 onFpsDropLevelCapping(event, data) { 22187 // Don't add a restricted level more than once 22188 const level = this.hls.levels[data.droppedLevel]; 22189 if (this.isLevelAllowed(level)) { 22190 this.restrictedLevels.push({ 22191 bitrate: level.bitrate, 22192 height: level.height, 22193 width: level.width 22194 }); 22195 } 22196 } 22197 onMediaAttaching(event, data) { 22198 this.media = data.media instanceof HTMLVideoElement ? data.media : null; 22199 this.clientRect = null; 22200 if (this.timer && this.hls.levels.length) { 22201 this.detectPlayerSize(); 22202 } 22203 } 22204 onManifestParsed(event, data) { 22205 const hls = this.hls; 22206 this.restrictedLevels = []; 22207 this.firstLevel = data.firstLevel; 22208 if (hls.config.capLevelToPlayerSize && data.video) { 22209 // Start capping immediately if the manifest has signaled video codecs 22210 this.startCapping(); 22211 } 22212 } 22213 onLevelsUpdated(event, data) { 22214 if (this.timer && isFiniteNumber(this.autoLevelCapping)) { 22215 this.detectPlayerSize(); 22216 } 22217 } 22218 22219 // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted 22220 // to the first level 22221 onBufferCodecs(event, data) { 22222 const hls = this.hls; 22223 if (hls.config.capLevelToPlayerSize && data.video) { 22224 // If the manifest did not signal a video codec capping has been deferred until we're certain video is present 22225 this.startCapping(); 22226 } 22227 } 22228 onMediaDetaching() { 22229 this.stopCapping(); 22230 } 22231 detectPlayerSize() { 22232 if (this.media) { 22233 if (this.mediaHeight <= 0 || this.mediaWidth <= 0) { 22234 this.clientRect = null; 22235 return; 22236 } 22237 const levels = this.hls.levels; 22238 if (levels.length) { 22239 const hls = this.hls; 22240 const maxLevel = this.getMaxLevel(levels.length - 1); 22241 if (maxLevel !== this.autoLevelCapping) { 22242 logger.log(`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`); 22243 } 22244 hls.autoLevelCapping = maxLevel; 22245 if (hls.autoLevelCapping > this.autoLevelCapping && this.streamController) { 22246 // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch 22247 // usually happen when the user go to the fullscreen mode. 22248 this.streamController.nextLevelSwitch(); 22249 } 22250 this.autoLevelCapping = hls.autoLevelCapping; 22251 } 22252 } 22253 } 22254 22255 /* 22256 * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled) 22257 */ 22258 getMaxLevel(capLevelIndex) { 22259 const levels = this.hls.levels; 22260 if (!levels.length) { 22261 return -1; 22262 } 22263 const validLevels = levels.filter((level, index) => this.isLevelAllowed(level) && index <= capLevelIndex); 22264 this.clientRect = null; 22265 return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight); 22266 } 22267 startCapping() { 22268 if (this.timer) { 22269 // Don't reset capping if started twice; this can happen if the manifest signals a video codec 22270 return; 22271 } 22272 this.autoLevelCapping = Number.POSITIVE_INFINITY; 22273 self.clearInterval(this.timer); 22274 this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000); 22275 this.detectPlayerSize(); 22276 } 22277 stopCapping() { 22278 this.restrictedLevels = []; 22279 this.firstLevel = -1; 22280 this.autoLevelCapping = Number.POSITIVE_INFINITY; 22281 if (this.timer) { 22282 self.clearInterval(this.timer); 22283 this.timer = undefined; 22284 } 22285 } 22286 getDimensions() { 22287 if (this.clientRect) { 22288 return this.clientRect; 22289 } 22290 const media = this.media; 22291 const boundsRect = { 22292 width: 0, 22293 height: 0 22294 }; 22295 if (media) { 22296 const clientRect = media.getBoundingClientRect(); 22297 boundsRect.width = clientRect.width; 22298 boundsRect.height = clientRect.height; 22299 if (!boundsRect.width && !boundsRect.height) { 22300 // When the media element has no width or height (equivalent to not being in the DOM), 22301 // then use its width and height attributes (media.width, media.height) 22302 boundsRect.width = clientRect.right - clientRect.left || media.width || 0; 22303 boundsRect.height = clientRect.bottom - clientRect.top || media.height || 0; 22304 } 22305 } 22306 this.clientRect = boundsRect; 22307 return boundsRect; 22308 } 22309 get mediaWidth() { 22310 return this.getDimensions().width * this.contentScaleFactor; 22311 } 22312 get mediaHeight() { 22313 return this.getDimensions().height * this.contentScaleFactor; 22314 } 22315 get contentScaleFactor() { 22316 let pixelRatio = 1; 22317 if (!this.hls.config.ignoreDevicePixelRatio) {
vendor: 5,064 bytes, lines 22318-22454
22318 try { 22319 pixelRatio = self.devicePixelRatio; 22320 } catch (e) { 22321 /* no-op */ 22322 } 22323 } 22324 return pixelRatio; 22325 } 22326 isLevelAllowed(level) { 22327 const restrictedLevels = this.restrictedLevels; 22328 return !restrictedLevels.some(restrictedLevel => { 22329 return level.bitrate === restrictedLevel.bitrate && level.width === restrictedLevel.width && level.height === restrictedLevel.height; 22330 }); 22331 } 22332 static getMaxLevelByMediaSize(levels, width, height) { 22333 if (!(levels != null && levels.length)) { 22334 return -1; 22335 } 22336 22337 // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next 22338 // to determine whether we've chosen the greatest bandwidth for the media's dimensions 22339 const atGreatestBandwidth = (curLevel, nextLevel) => { 22340 if (!nextLevel) { 22341 return true; 22342 } 22343 return curLevel.width !== nextLevel.width || curLevel.height !== nextLevel.height; 22344 }; 22345 22346 // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to 22347 // the max level 22348 let maxLevelIndex = levels.length - 1; 22349 // Prevent changes in aspect-ratio from causing capping to toggle back and forth 22350 const squareSize = Math.max(width, height); 22351 for (let i = 0; i < levels.length; i += 1) { 22352 const level = levels[i]; 22353 if ((level.width >= squareSize || level.height >= squareSize) && atGreatestBandwidth(level, levels[i + 1])) { 22354 maxLevelIndex = i; 22355 break; 22356 } 22357 } 22358 return maxLevelIndex; 22359 } 22360} 22361 22362class FPSController { 22363 constructor(hls) { 22364 this.hls = void 0; 22365 this.isVideoPlaybackQualityAvailable = false; 22366 this.timer = void 0; 22367 this.media = null; 22368 this.lastTime = void 0; 22369 this.lastDroppedFrames = 0; 22370 this.lastDecodedFrames = 0; 22371 // stream controller must be provided as a dependency! 22372 this.streamController = void 0; 22373 this.hls = hls; 22374 this.registerListeners(); 22375 } 22376 setStreamController(streamController) { 22377 this.streamController = streamController; 22378 } 22379 registerListeners() { 22380 this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 22381 } 22382 unregisterListeners() { 22383 this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 22384 } 22385 destroy() { 22386 if (this.timer) { 22387 clearInterval(this.timer); 22388 } 22389 this.unregisterListeners(); 22390 this.isVideoPlaybackQualityAvailable = false; 22391 this.media = null; 22392 } 22393 onMediaAttaching(event, data) { 22394 const config = this.hls.config; 22395 if (config.capLevelOnFPSDrop) { 22396 const media = data.media instanceof self.HTMLVideoElement ? data.media : null; 22397 this.media = media; 22398 if (media && typeof media.getVideoPlaybackQuality === 'function') { 22399 this.isVideoPlaybackQualityAvailable = true; 22400 } 22401 self.clearInterval(this.timer); 22402 this.timer = self.setInterval(this.checkFPSInterval.bind(this), config.fpsDroppedMonitoringPeriod); 22403 } 22404 } 22405 checkFPS(video, decodedFrames, droppedFrames) { 22406 const currentTime = performance.now(); 22407 if (decodedFrames) { 22408 if (this.lastTime) { 22409 const currentPeriod = currentTime - this.lastTime; 22410 const currentDropped = droppedFrames - this.lastDroppedFrames; 22411 const currentDecoded = decodedFrames - this.lastDecodedFrames; 22412 const droppedFPS = 1000 * currentDropped / currentPeriod; 22413 const hls = this.hls; 22414 hls.trigger(Events.FPS_DROP, { 22415 currentDropped: currentDropped, 22416 currentDecoded: currentDecoded, 22417 totalDroppedFrames: droppedFrames 22418 }); 22419 if (droppedFPS > 0) { 22420 // logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod)); 22421 if (currentDropped > hls.config.fpsDroppedMonitoringThreshold * currentDecoded) { 22422 let currentLevel = hls.currentLevel; 22423 logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel); 22424 if (currentLevel > 0 && (hls.autoLevelCapping === -1 || hls.autoLevelCapping >= currentLevel)) { 22425 currentLevel = currentLevel - 1; 22426 hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, { 22427 level: currentLevel, 22428 droppedLevel: hls.currentLevel 22429 }); 22430 hls.autoLevelCapping = currentLevel; 22431 this.streamController.nextLevelSwitch(); 22432 } 22433 } 22434 } 22435 } 22436 this.lastTime = currentTime; 22437 this.lastDroppedFrames = droppedFrames; 22438 this.lastDecodedFrames = decodedFrames; 22439 } 22440 } 22441 checkFPSInterval() { 22442 const video = this.media; 22443 if (video) { 22444 if (this.isVideoPlaybackQualityAvailable) { 22445 const videoPlaybackQuality = video.getVideoPlaybackQuality(); 22446 this.checkFPS(video, videoPlaybackQuality.totalVideoFrames, videoPlaybackQuality.droppedVideoFrames); 22447 } else { 22448 // HTMLVideoElement doesn't include the webkit types 22449 this.checkFPS(video, video.webkitDecodedFrameCount, video.webkitDroppedFrameCount); 22450 } 22451 } 22452 } 22453} 22454
vendor: 5,040 bytes, lines 22455-22582
22455const LOGGER_PREFIX = '[eme]'; 22456/** 22457 * Controller to deal with encrypted media extensions (EME) 22458 * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API 22459 * 22460 * @class 22461 * @constructor 22462 */ 22463class EMEController { 22464 constructor(hls) { 22465 this.hls = void 0; 22466 this.config = void 0; 22467 this.media = null; 22468 this.keyFormatPromise = null; 22469 this.keySystemAccessPromises = {}; 22470 this._requestLicenseFailureCount = 0; 22471 this.mediaKeySessions = []; 22472 this.keyIdToKeySessionPromise = {}; 22473 this.setMediaKeysQueue = EMEController.CDMCleanupPromise ? [EMEController.CDMCleanupPromise] : []; 22474 this.onMediaEncrypted = this._onMediaEncrypted.bind(this); 22475 this.onWaitingForKey = this._onWaitingForKey.bind(this); 22476 this.debug = logger.debug.bind(logger, LOGGER_PREFIX); 22477 this.log = logger.log.bind(logger, LOGGER_PREFIX); 22478 this.warn = logger.warn.bind(logger, LOGGER_PREFIX); 22479 this.error = logger.error.bind(logger, LOGGER_PREFIX); 22480 this.hls = hls; 22481 this.config = hls.config; 22482 this.registerListeners(); 22483 } 22484 destroy() { 22485 this.unregisterListeners(); 22486 this.onMediaDetached(); 22487 // Remove any references that could be held in config options or callbacks 22488 const config = this.config; 22489 config.requestMediaKeySystemAccessFunc = null; 22490 config.licenseXhrSetup = config.licenseResponseCallback = undefined; 22491 config.drmSystems = config.drmSystemOptions = {}; 22492 // @ts-ignore 22493 this.hls = this.onMediaEncrypted = this.onWaitingForKey = this.keyIdToKeySessionPromise = null; 22494 // @ts-ignore 22495 this.config = null; 22496 } 22497 registerListeners() { 22498 this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 22499 this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); 22500 this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 22501 this.hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 22502 } 22503 unregisterListeners() { 22504 this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 22505 this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); 22506 this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 22507 this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 22508 } 22509 getLicenseServerUrl(keySystem) { 22510 const { 22511 drmSystems, 22512 widevineLicenseUrl 22513 } = this.config; 22514 const keySystemConfiguration = drmSystems[keySystem]; 22515 if (keySystemConfiguration) { 22516 return keySystemConfiguration.licenseUrl; 22517 } 22518 22519 // For backward compatibility 22520 if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) { 22521 return widevineLicenseUrl; 22522 } 22523 throw new Error(`no license server URL configured for key-system "${keySystem}"`); 22524 } 22525 getServerCertificateUrl(keySystem) { 22526 const { 22527 drmSystems 22528 } = this.config; 22529 const keySystemConfiguration = drmSystems[keySystem]; 22530 if (keySystemConfiguration) { 22531 return keySystemConfiguration.serverCertificateUrl; 22532 } else { 22533 this.log(`No Server Certificate in config.drmSystems["${keySystem}"]`); 22534 } 22535 } 22536 attemptKeySystemAccess(keySystemsToAttempt) { 22537 const levels = this.hls.levels; 22538 const uniqueCodec = (value, i, a) => !!value && a.indexOf(value) === i; 22539 const audioCodecs = levels.map(level => level.audioCodec).filter(uniqueCodec); 22540 const videoCodecs = levels.map(level => level.videoCodec).filter(uniqueCodec); 22541 if (audioCodecs.length + videoCodecs.length === 0) { 22542 videoCodecs.push('avc1.42e01e'); 22543 } 22544 return new Promise((resolve, reject) => { 22545 const attempt = keySystems => { 22546 const keySystem = keySystems.shift(); 22547 this.getMediaKeysPromise(keySystem, audioCodecs, videoCodecs).then(mediaKeys => resolve({ 22548 keySystem, 22549 mediaKeys 22550 })).catch(error => { 22551 if (keySystems.length) { 22552 attempt(keySystems); 22553 } else if (error instanceof EMEKeyError) { 22554 reject(error); 22555 } else { 22556 reject(new EMEKeyError({ 22557 type: ErrorTypes.KEY_SYSTEM_ERROR, 22558 details: ErrorDetails.KEY_SYSTEM_NO_ACCESS, 22559 error, 22560 fatal: true 22561 }, error.message)); 22562 } 22563 }); 22564 }; 22565 attempt(keySystemsToAttempt); 22566 }); 22567 } 22568 requestMediaKeySystemAccess(keySystem, supportedConfigurations) { 22569 const { 22570 requestMediaKeySystemAccessFunc 22571 } = this.config; 22572 if (!(typeof requestMediaKeySystemAccessFunc === 'function')) { 22573 let errMessage = `Configured requestMediaKeySystemAccess is not a function ${requestMediaKeySystemAccessFunc}`; 22574 if (requestMediaKeySystemAccess === null && self.location.protocol === 'http:') { 22575 errMessage = `navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`; 22576 } 22577 return Promise.reject(new Error(errMessage)); 22578 } 22579 return requestMediaKeySystemAccessFunc(keySystem, supportedConfigurations); 22580 } 22581 getMediaKeysPromise(keySystem, audioCodecs, videoCodecs) { 22582 // This can throw, but is caught in event handler callpath
22583 const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, this.config.drmSystemOptions); 22584 const keySystemAccessPromises = this.keySystemAccessPromises[keySystem]; 22585 let keySystemAccess = keySystemAccessPromises == null ? void 0 : keySystemAccessPromises.keySystemAccess; 22586 if (!keySystemAccess) { 22587 this.log(`Requesting encrypted media "${keySystem}" key-system access with config: ${JSON.stringify(mediaKeySystemConfigs)}`); 22588 keySystemAccess = this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs); 22589 const _keySystemAccessPromises = this.keySystemAccessPromises[keySystem] = { 22590 keySystemAccess 22591 }; 22592 keySystemAccess.catch(error => { 22593 this.log(`Failed to obtain access to key-system "${keySystem}": ${error}`); 22594 }); 22595 return keySystemAccess.then(mediaKeySystemAccess => { 22596 this.log(`Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`); 22597 const certificateRequest = this.fetchServerCertificate(keySystem); 22598 this.log(`Create media-keys for "${keySystem}"`); 22599 _keySystemAccessPromises.mediaKeys = mediaKeySystemAccess.createMediaKeys().then(mediaKeys => { 22600 this.log(`Media-keys created for "${keySystem}"`); 22601 return certificateRequest.then(certificate => { 22602 if (certificate) { 22603 return this.setMediaKeysServerCertificate(mediaKeys, keySystem, certificate); 22604 } 22605 return mediaKeys; 22606 }); 22607 }); 22608 _keySystemAccessPromises.mediaKeys.catch(error => { 22609 this.error(`Failed to create media-keys for "${keySystem}"}: ${error}`); 22610 }); 22611 return _keySystemAccessPromises.mediaKeys; 22612 }); 22613 } 22614 return keySystemAccess.then(() => keySystemAccessPromises.mediaKeys); 22615 } 22616 createMediaKeySessionContext({ 22617 decryptdata, 22618 keySystem, 22619 mediaKeys 22620 }) { 22621 this.log(`Creating key-system session "${keySystem}" keyId: ${Hex.hexDump(decryptdata.keyId || [])}`); 22622 const mediaKeysSession = mediaKeys.createSession(); 22623 const mediaKeySessionContext = { 22624 decryptdata, 22625 keySystem, 22626 mediaKeys, 22627 mediaKeysSession, 22628 keyStatus: 'status-pending' 22629 }; 22630 this.mediaKeySessions.push(mediaKeySessionContext); 22631 return mediaKeySessionContext; 22632 } 22633 renewKeySession(mediaKeySessionContext) { 22634 const decryptdata = mediaKeySessionContext.decryptdata; 22635 if (decryptdata.pssh) { 22636 const keySessionContext = this.createMediaKeySessionContext(mediaKeySessionContext); 22637 const keyId = this.getKeyIdString(decryptdata); 22638 const scheme = 'cenc'; 22639 this.keyIdToKeySessionPromise[keyId] = this.generateRequestWithPreferredKeySession(keySessionContext, scheme, decryptdata.pssh, 'expired'); 22640 } else { 22641 this.warn(`Could not renew expired session. Missing pssh initData.`); 22642 } 22643 this.removeSession(mediaKeySessionContext); 22644 } 22645 getKeyIdString(decryptdata) { 22646 if (!decryptdata) { 22647 throw new Error('Could not read keyId of undefined decryptdata'); 22648 } 22649 if (decryptdata.keyId === null) { 22650 throw new Error('keyId is null'); 22651 } 22652 return Hex.hexDump(decryptdata.keyId); 22653 } 22654 updateKeySession(mediaKeySessionContext, data) { 22655 var _mediaKeySessionConte; 22656 const keySession = mediaKeySessionContext.mediaKeysSession; 22657 this.log(`Updating key-session "${keySession.sessionId}" for keyID ${Hex.hexDump(((_mediaKeySessionConte = mediaKeySessionContext.decryptdata) == null ? void 0 : _mediaKeySessionConte.keyId) || [])} 22658 } (data length: ${data ? data.byteLength : data})`); 22659 return keySession.update(data); 22660 } 22661 selectKeySystemFormat(frag) { 22662 const keyFormats = Object.keys(frag.levelkeys || {}); 22663 if (!this.keyFormatPromise) { 22664 this.log(`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${frag.level}) key formats ${keyFormats.join(', ')}`); 22665 this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); 22666 } 22667 return this.keyFormatPromise; 22668 } 22669 getKeyFormatPromise(keyFormats) { 22670 return new Promise((resolve, reject) => {
22671 const keySystemsInConfig = getKeySystemsForConfig(this.config); 22672 const keySystemsToAttempt = keyFormats.map(keySystemFormatToKeySystemDomain).filter(value => !!value && keySystemsInConfig.indexOf(value) !== -1); 22673 return this.getKeySystemSelectionPromise(keySystemsToAttempt).then(({ 22674 keySystem 22675 }) => { 22676 const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem); 22677 if (keySystemFormat) { 22678 resolve(keySystemFormat); 22679 } else { 22680 reject(new Error(`Unable to find format for key-system "${keySystem}"`)); 22681 } 22682 }).catch(reject); 22683 }); 22684 } 22685 loadKey(data) { 22686 const decryptdata = data.keyInfo.decryptdata; 22687 const keyId = this.getKeyIdString(decryptdata); 22688 const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`; 22689 this.log(`Starting session for key ${keyDetails}`); 22690 let keySessionContextPromise = this.keyIdToKeySessionPromise[keyId]; 22691 if (!keySessionContextPromise) { 22692 keySessionContextPromise = this.keyIdToKeySessionPromise[keyId] = this.getKeySystemForKeyPromise(decryptdata).then(({ 22693 keySystem, 22694 mediaKeys 22695 }) => { 22696 this.throwIfDestroyed(); 22697 this.log(`Handle encrypted media sn: ${data.frag.sn} ${data.frag.type}: ${data.frag.level} using key ${keyDetails}`); 22698 return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { 22699 this.throwIfDestroyed(); 22700 const keySessionContext = this.createMediaKeySessionContext({ 22701 keySystem, 22702 mediaKeys, 22703 decryptdata 22704 }); 22705 const scheme = 'cenc'; 22706 return this.generateRequestWithPreferredKeySession(keySessionContext, scheme, decryptdata.pssh, 'playlist-key'); 22707 }); 22708 }); 22709 keySessionContextPromise.catch(error => this.handleError(error)); 22710 } 22711 return keySessionContextPromise; 22712 } 22713 throwIfDestroyed(message = 'Invalid state') { 22714 if (!this.hls) { 22715 throw new Error('invalid state'); 22716 } 22717 } 22718 handleError(error) { 22719 if (!this.hls) { 22720 return; 22721 } 22722 this.error(error.message); 22723 if (error instanceof EMEKeyError) { 22724 this.hls.trigger(Events.ERROR, error.data); 22725 } else { 22726 this.hls.trigger(Events.ERROR, { 22727 type: ErrorTypes.KEY_SYSTEM_ERROR, 22728 details: ErrorDetails.KEY_SYSTEM_NO_KEYS, 22729 error, 22730 fatal: true 22731 }); 22732 } 22733 } 22734 getKeySystemForKeyPromise(decryptdata) { 22735 const keyId = this.getKeyIdString(decryptdata); 22736 const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId]; 22737 if (!mediaKeySessionContext) { 22738 const keySystem = keySystemFormatToKeySystemDomain(decryptdata.keyFormat); 22739 const keySystemsToAttempt = keySystem ? [keySystem] : getKeySystemsForConfig(this.config); 22740 return this.attemptKeySystemAccess(keySystemsToAttempt); 22741 } 22742 return mediaKeySessionContext; 22743 } 22744 getKeySystemSelectionPromise(keySystemsToAttempt) { 22745 if (!keySystemsToAttempt.length) { 22746 keySystemsToAttempt = getKeySystemsForConfig(this.config); 22747 } 22748 if (keySystemsToAttempt.length === 0) { 22749 throw new EMEKeyError({ 22750 type: ErrorTypes.KEY_SYSTEM_ERROR, 22751 details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE, 22752 fatal: true 22753 }, `Missing key-system license configuration options ${JSON.stringify({ 22754 drmSystems: this.config.drmSystems 22755 })}`); 22756 } 22757 return this.attemptKeySystemAccess(keySystemsToAttempt); 22758 } 22759 _onMediaEncrypted(event) { 22760 const { 22761 initDataType, 22762 initData 22763 } = event; 22764 this.debug(`"${event.type}" event: init data type: "${initDataType}"`); 22765 22766 // Ignore event when initData is null 22767 if (initData === null) { 22768 return; 22769 } 22770 let keyId; 22771 let keySystemDomain; 22772 if (initDataType === 'sinf' && this.config.drmSystems[KeySystems.FAIRPLAY]) { 22773 // Match sinf keyId to playlist skd://keyId= 22774 const json = bin2str(new Uint8Array(initData)); 22775 try { 22776 const sinf = base64Decode(JSON.parse(json).sinf); 22777 const tenc = parseSinf(new Uint8Array(sinf)); 22778 if (!tenc) { 22779 return; 22780 } 22781 keyId = tenc.subarray(8, 24); 22782 keySystemDomain = KeySystems.FAIRPLAY; 22783 } catch (error) { 22784 this.warn('Failed to parse sinf "encrypted" event message initData'); 22785 return; 22786 } 22787 } else {
22788 // Support clear-lead key-session creation (otherwise depend on playlist keys) 22789 const psshInfo = parsePssh(initData); 22790 if (psshInfo === null) { 22791 return; 22792 } 22793 if (psshInfo.version === 0 && psshInfo.systemId === KeySystemIds.WIDEVINE && psshInfo.data) { 22794 keyId = psshInfo.data.subarray(8, 24); 22795 } 22796 keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId); 22797 } 22798 if (!keySystemDomain || !keyId) { 22799 return; 22800 } 22801 const keyIdHex = Hex.hexDump(keyId); 22802 const { 22803 keyIdToKeySessionPromise, 22804 mediaKeySessions 22805 } = this; 22806 let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; 22807 for (let i = 0; i < mediaKeySessions.length; i++) { 22808 // Match playlist key 22809 const keyContext = mediaKeySessions[i]; 22810 const decryptdata = keyContext.decryptdata; 22811 if (decryptdata.pssh || !decryptdata.keyId) { 22812 continue; 22813 } 22814 const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); 22815 if (keyIdHex === oldKeyIdHex || decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1) { 22816 keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; 22817 delete keyIdToKeySessionPromise[oldKeyIdHex]; 22818 decryptdata.pssh = new Uint8Array(initData); 22819 decryptdata.keyId = keyId; 22820 keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = keySessionContextPromise.then(() => { 22821 return this.generateRequestWithPreferredKeySession(keyContext, initDataType, initData, 'encrypted-event-key-match'); 22822 }); 22823 break; 22824 } 22825 } 22826 if (!keySessionContextPromise) { 22827 // Clear-lead key (not encountered in playlist) 22828 keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = this.getKeySystemSelectionPromise([keySystemDomain]).then(({ 22829 keySystem, 22830 mediaKeys 22831 }) => { 22832 var _keySystemToKeySystem; 22833 this.throwIfDestroyed(); 22834 const decryptdata = new LevelKey('ISO-23001-7', keyIdHex, (_keySystemToKeySystem = keySystemDomainToKeySystemFormat(keySystem)) != null ? _keySystemToKeySystem : ''); 22835 decryptdata.pssh = new Uint8Array(initData); 22836 decryptdata.keyId = keyId; 22837 return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { 22838 this.throwIfDestroyed(); 22839 const keySessionContext = this.createMediaKeySessionContext({ 22840 decryptdata, 22841 keySystem, 22842 mediaKeys 22843 }); 22844 return this.generateRequestWithPreferredKeySession(keySessionContext, initDataType, initData, 'encrypted-event-no-match'); 22845 }); 22846 }); 22847 } 22848 keySessionContextPromise.catch(error => this.handleError(error)); 22849 } 22850 _onWaitingForKey(event) { 22851 this.log(`"${event.type}" event`); 22852 } 22853 attemptSetMediaKeys(keySystem, mediaKeys) { 22854 const queue = this.setMediaKeysQueue.slice(); 22855 this.log(`Setting media-keys for "${keySystem}"`); 22856 // Only one setMediaKeys() can run at one time, and multiple setMediaKeys() operations 22857 // can be queued for execution for multiple key sessions. 22858 const setMediaKeysPromise = Promise.all(queue).then(() => { 22859 if (!this.media) { 22860 throw new Error('Attempted to set mediaKeys without media element attached'); 22861 } 22862 return this.media.setMediaKeys(mediaKeys); 22863 }); 22864 this.setMediaKeysQueue.push(setMediaKeysPromise); 22865 return setMediaKeysPromise.then(() => { 22866 this.log(`Media-keys set for "${keySystem}"`); 22867 queue.push(setMediaKeysPromise); 22868 this.setMediaKeysQueue = this.setMediaKeysQueue.filter(p => queue.indexOf(p) === -1); 22869 }); 22870 } 22871 generateRequestWithPreferredKeySession(context, initDataType, initData, reason) { 22872 var _this$config$drmSyste, _this$config$drmSyste2; 22873 const generateRequestFilter = (_this$config$drmSyste = this.config.drmSystems) == null ? void 0 : (_this$config$drmSyste2 = _this$config$drmSyste[context.keySystem]) == null ? void 0 : _this$config$drmSyste2.generateRequest; 22874 if (generateRequestFilter) { 22875 try { 22876 const mappedInitData = generateRequestFilter.call(this.hls, initDataType, initData, context); 22877 if (!mappedInitData) { 22878 throw new Error('Invalid response from configured generateRequest filter'); 22879 } 22880 initDataType = mappedInitData.initDataType; 22881 initData = context.decryptdata.pssh = mappedInitData.initData ? new Uint8Array(mappedInitData.initData) : null; 22882 } catch (error) { 22883 var _this$hls; 22884 this.warn(error.message); 22885 if ((_this$hls = this.hls) != null && _this$hls.config.debug) { 22886 throw error; 22887 } 22888 } 22889 } 22890 if (initData === null) { 22891 this.log(`Skipping key-session request for "${reason}" (no initData)`); 22892 return Promise.resolve(context); 22893 }
22894 const keyId = this.getKeyIdString(context.decryptdata); 22895 this.log(`Generating key-session request for "${reason}": ${keyId} (init data type: ${initDataType} length: ${initData ? initData.byteLength : null})`); 22896 const licenseStatus = new EventEmitter(); 22897 const onmessage = context._onmessage = event => { 22898 const keySession = context.mediaKeysSession; 22899 if (!keySession) { 22900 licenseStatus.emit('error', new Error('invalid state')); 22901 return; 22902 } 22903 const { 22904 messageType, 22905 message 22906 } = event; 22907 this.log(`"${messageType}" message event for session "${keySession.sessionId}" message size: ${message.byteLength}`); 22908 if (messageType === 'license-request' || messageType === 'license-renewal') { 22909 this.renewLicense(context, message).catch(error => { 22910 this.handleError(error); 22911 licenseStatus.emit('error', error); 22912 }); 22913 } else if (messageType === 'license-release') { 22914 if (context.keySystem === KeySystems.FAIRPLAY) { 22915 this.updateKeySession(context, strToUtf8array('acknowledged')); 22916 this.removeSession(context); 22917 } 22918 } else { 22919 this.warn(`unhandled media key message type "${messageType}"`); 22920 } 22921 }; 22922 const onkeystatuseschange = context._onkeystatuseschange = event => { 22923 const keySession = context.mediaKeysSession; 22924 if (!keySession) { 22925 licenseStatus.emit('error', new Error('invalid state')); 22926 return; 22927 } 22928 this.onKeyStatusChange(context); 22929 const keyStatus = context.keyStatus; 22930 licenseStatus.emit('keyStatus', keyStatus); 22931 if (keyStatus === 'expired') { 22932 this.warn(`${context.keySystem} expired for key ${keyId}`); 22933 this.renewKeySession(context); 22934 } 22935 }; 22936 context.mediaKeysSession.addEventListener('message', onmessage); 22937 context.mediaKeysSession.addEventListener('keystatuseschange', onkeystatuseschange); 22938 const keyUsablePromise = new Promise((resolve, reject) => { 22939 licenseStatus.on('error', reject); 22940 licenseStatus.on('keyStatus', keyStatus => { 22941 if (keyStatus.startsWith('usable')) { 22942 resolve(); 22943 } else if (keyStatus === 'output-restricted') { 22944 reject(new EMEKeyError({ 22945 type: ErrorTypes.KEY_SYSTEM_ERROR, 22946 details: ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED, 22947 fatal: false 22948 }, 'HDCP level output restricted')); 22949 } else if (keyStatus === 'internal-error') { 22950 reject(new EMEKeyError({ 22951 type: ErrorTypes.KEY_SYSTEM_ERROR, 22952 details: ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR, 22953 fatal: true 22954 }, `key status changed to "${keyStatus}"`)); 22955 } else if (keyStatus === 'expired') { 22956 reject(new Error('key expired while generating request')); 22957 } else { 22958 this.warn(`unhandled key status change "${keyStatus}"`); 22959 } 22960 }); 22961 }); 22962 return context.mediaKeysSession.generateRequest(initDataType, initData).then(() => { 22963 var _context$mediaKeysSes; 22964 this.log(`Request generated for key-session "${(_context$mediaKeysSes = context.mediaKeysSession) == null ? void 0 : _context$mediaKeysSes.sessionId}" keyId: ${keyId}`); 22965 }).catch(error => { 22966 throw new EMEKeyError({ 22967 type: ErrorTypes.KEY_SYSTEM_ERROR, 22968 details: ErrorDetails.KEY_SYSTEM_NO_SESSION, 22969 error, 22970 fatal: false 22971 }, `Error generating key-session request: ${error}`); 22972 }).then(() => keyUsablePromise).catch(error => { 22973 licenseStatus.removeAllListeners(); 22974 this.removeSession(context); 22975 throw error; 22976 }).then(() => { 22977 licenseStatus.removeAllListeners(); 22978 return context; 22979 }); 22980 } 22981 onKeyStatusChange(mediaKeySessionContext) { 22982 mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach((status, keyId) => { 22983 this.log(`key status change "${status}" for keyStatuses keyId: ${Hex.hexDump('buffer' in keyId ? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength) : new Uint8Array(keyId))} session keyId: ${Hex.hexDump(new Uint8Array(mediaKeySessionContext.decryptdata.keyId || []))} uri: ${mediaKeySessionContext.decryptdata.uri}`); 22984 mediaKeySessionContext.keyStatus = status; 22985 }); 22986 } 22987 fetchServerCertificate(keySystem) { 22988 const config = this.config; 22989 const Loader = config.loader;
vendor: 9,755 bytes, lines 22990-23231
22990 const certLoader = new Loader(config); 22991 const url = this.getServerCertificateUrl(keySystem); 22992 if (!url) { 22993 return Promise.resolve(); 22994 } 22995 this.log(`Fetching server certificate for "${keySystem}"`); 22996 return new Promise((resolve, reject) => { 22997 const loaderContext = { 22998 responseType: 'arraybuffer', 22999 url 23000 }; 23001 const loadPolicy = config.certLoadPolicy.default; 23002 const loaderConfig = { 23003 loadPolicy, 23004 timeout: loadPolicy.maxLoadTimeMs, 23005 maxRetry: 0, 23006 retryDelay: 0, 23007 maxRetryDelay: 0 23008 }; 23009 const loaderCallbacks = { 23010 onSuccess: (response, stats, context, networkDetails) => { 23011 resolve(response.data); 23012 }, 23013 onError: (response, contex, networkDetails, stats) => { 23014 reject(new EMEKeyError({ 23015 type: ErrorTypes.KEY_SYSTEM_ERROR, 23016 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, 23017 fatal: true, 23018 networkDetails, 23019 response: _objectSpread2({ 23020 url: loaderContext.url, 23021 data: undefined 23022 }, response) 23023 }, `"${keySystem}" certificate request failed (${url}). Status: ${response.code} (${response.text})`)); 23024 }, 23025 onTimeout: (stats, context, networkDetails) => { 23026 reject(new EMEKeyError({ 23027 type: ErrorTypes.KEY_SYSTEM_ERROR, 23028 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, 23029 fatal: true, 23030 networkDetails, 23031 response: { 23032 url: loaderContext.url, 23033 data: undefined 23034 } 23035 }, `"${keySystem}" certificate request timed out (${url})`)); 23036 }, 23037 onAbort: (stats, context, networkDetails) => { 23038 reject(new Error('aborted')); 23039 } 23040 }; 23041 certLoader.load(loaderContext, loaderConfig, loaderCallbacks); 23042 }); 23043 } 23044 setMediaKeysServerCertificate(mediaKeys, keySystem, cert) { 23045 return new Promise((resolve, reject) => { 23046 mediaKeys.setServerCertificate(cert).then(success => { 23047 this.log(`setServerCertificate ${success ? 'success' : 'not supported by CDM'} (${cert == null ? void 0 : cert.byteLength}) on "${keySystem}"`); 23048 resolve(mediaKeys); 23049 }).catch(error => { 23050 reject(new EMEKeyError({ 23051 type: ErrorTypes.KEY_SYSTEM_ERROR, 23052 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED, 23053 error, 23054 fatal: true 23055 }, error.message)); 23056 }); 23057 }); 23058 } 23059 renewLicense(context, keyMessage) { 23060 return this.requestLicense(context, new Uint8Array(keyMessage)).then(data => { 23061 return this.updateKeySession(context, new Uint8Array(data)).catch(error => { 23062 throw new EMEKeyError({ 23063 type: ErrorTypes.KEY_SYSTEM_ERROR, 23064 details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED, 23065 error, 23066 fatal: true 23067 }, error.message); 23068 }); 23069 }); 23070 } 23071 unpackPlayReadyKeyMessage(xhr, licenseChallenge) { 23072 // On Edge, the raw license message is UTF-16-encoded XML. We need 23073 // to unpack the Challenge element (base64-encoded string containing the 23074 // actual license request) and any HttpHeader elements (sent as request 23075 // headers). 23076 // For PlayReady CDMs, we need to dig the Challenge out of the XML. 23077 const xmlString = String.fromCharCode.apply(null, new Uint16Array(licenseChallenge.buffer)); 23078 if (!xmlString.includes('PlayReadyKeyMessage')) { 23079 // This does not appear to be a wrapped message as on Edge. Some 23080 // clients do not need this unwrapping, so we will assume this is one of 23081 // them. Note that "xml" at this point probably looks like random 23082 // garbage, since we interpreted UTF-8 as UTF-16. 23083 xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8'); 23084 return licenseChallenge; 23085 } 23086 const keyMessageXml = new DOMParser().parseFromString(xmlString, 'application/xml'); 23087 // Set request headers. 23088 const headers = keyMessageXml.querySelectorAll('HttpHeader'); 23089 if (headers.length > 0) { 23090 let header; 23091 for (let i = 0, len = headers.length; i < len; i++) { 23092 var _header$querySelector, _header$querySelector2; 23093 header = headers[i]; 23094 const name = (_header$querySelector = header.querySelector('name')) == null ? void 0 : _header$querySelector.textContent; 23095 const value = (_header$querySelector2 = header.querySelector('value')) == null ? void 0 : _header$querySelector2.textContent; 23096 if (name && value) { 23097 xhr.setRequestHeader(name, value); 23098 } 23099 } 23100 } 23101 const challengeElement = keyMessageXml.querySelector('Challenge'); 23102 const challengeText = challengeElement == null ? void 0 : challengeElement.textContent; 23103 if (!challengeText) { 23104 throw new Error(`Cannot find <Challenge> in key message`); 23105 } 23106 return strToUtf8array(atob(challengeText)); 23107 } 23108 setupLicenseXHR(xhr, url, keysListItem, licenseChallenge) { 23109 const licenseXhrSetup = this.config.licenseXhrSetup; 23110 if (!licenseXhrSetup) { 23111 xhr.open('POST', url, true); 23112 return Promise.resolve({ 23113 xhr, 23114 licenseChallenge 23115 }); 23116 } 23117 return Promise.resolve().then(() => { 23118 if (!keysListItem.decryptdata) { 23119 throw new Error('Key removed'); 23120 } 23121 return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); 23122 }).catch(error => { 23123 if (!keysListItem.decryptdata) { 23124 // Key session removed. Cancel license request. 23125 throw error; 23126 } 23127 // let's try to open before running setup 23128 xhr.open('POST', url, true); 23129 return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); 23130 }).then(licenseXhrSetupResult => { 23131 // if licenseXhrSetup did not yet call open, let's do it now 23132 if (!xhr.readyState) { 23133 xhr.open('POST', url, true); 23134 } 23135 const finalLicenseChallenge = licenseXhrSetupResult ? licenseXhrSetupResult : licenseChallenge; 23136 return { 23137 xhr, 23138 licenseChallenge: finalLicenseChallenge 23139 }; 23140 }); 23141 } 23142 requestLicense(keySessionContext, licenseChallenge) { 23143 const keyLoadPolicy = this.config.keyLoadPolicy.default; 23144 return new Promise((resolve, reject) => { 23145 const url = this.getLicenseServerUrl(keySessionContext.keySystem); 23146 this.log(`Sending license request to URL: ${url}`); 23147 const xhr = new XMLHttpRequest(); 23148 xhr.responseType = 'arraybuffer'; 23149 xhr.onreadystatechange = () => { 23150 if (!this.hls || !keySessionContext.mediaKeysSession) { 23151 return reject(new Error('invalid state')); 23152 } 23153 if (xhr.readyState === 4) { 23154 if (xhr.status === 200) { 23155 this._requestLicenseFailureCount = 0; 23156 let data = xhr.response; 23157 this.log(`License received ${data instanceof ArrayBuffer ? data.byteLength : data}`); 23158 const licenseResponseCallback = this.config.licenseResponseCallback; 23159 if (licenseResponseCallback) { 23160 try { 23161 data = licenseResponseCallback.call(this.hls, xhr, url, keySessionContext); 23162 } catch (error) { 23163 this.error(error); 23164 } 23165 } 23166 resolve(data); 23167 } else { 23168 const retryConfig = keyLoadPolicy.errorRetry; 23169 const maxNumRetry = retryConfig ? retryConfig.maxNumRetry : 0; 23170 this._requestLicenseFailureCount++; 23171 if (this._requestLicenseFailureCount > maxNumRetry || xhr.status >= 400 && xhr.status < 500) { 23172 reject(new EMEKeyError({ 23173 type: ErrorTypes.KEY_SYSTEM_ERROR, 23174 details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED, 23175 fatal: true, 23176 networkDetails: xhr, 23177 response: { 23178 url, 23179 data: undefined, 23180 code: xhr.status, 23181 text: xhr.statusText 23182 } 23183 }, `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`)); 23184 } else { 23185 const attemptsLeft = maxNumRetry - this._requestLicenseFailureCount + 1; 23186 this.warn(`Retrying license request, ${attemptsLeft} attempts left`); 23187 this.requestLicense(keySessionContext, licenseChallenge).then(resolve, reject); 23188 } 23189 } 23190 } 23191 }; 23192 if (keySessionContext.licenseXhr && keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE) { 23193 keySessionContext.licenseXhr.abort(); 23194 } 23195 keySessionContext.licenseXhr = xhr; 23196 this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge).then(({ 23197 xhr, 23198 licenseChallenge 23199 }) => { 23200 if (keySessionContext.keySystem == KeySystems.PLAYREADY) { 23201 licenseChallenge = this.unpackPlayReadyKeyMessage(xhr, licenseChallenge); 23202 } 23203 xhr.send(licenseChallenge); 23204 }); 23205 }); 23206 } 23207 onMediaAttached(event, data) { 23208 if (!this.config.emeEnabled) { 23209 return; 23210 } 23211 const media = data.media; 23212 23213 // keep reference of media 23214 this.media = media; 23215 media.addEventListener('encrypted', this.onMediaEncrypted); 23216 media.addEventListener('waitingforkey', this.onWaitingForKey); 23217 } 23218 onMediaDetached() { 23219 const media = this.media; 23220 const mediaKeysList = this.mediaKeySessions; 23221 if (media) { 23222 media.removeEventListener('encrypted', this.onMediaEncrypted); 23223 media.removeEventListener('waitingforkey', this.onWaitingForKey); 23224 this.media = null; 23225 } 23226 this._requestLicenseFailureCount = 0; 23227 this.setMediaKeysQueue = []; 23228 this.mediaKeySessions = []; 23229 this.keyIdToKeySessionPromise = {}; 23230 LevelKey.clearKeyUriToKeyIdMap(); 23231
23232 // Close all sessions and remove media keys from the video element. 23233 const keySessionCount = mediaKeysList.length; 23234 EMEController.CDMCleanupPromise = Promise.all(mediaKeysList.map(mediaKeySessionContext => this.removeSession(mediaKeySessionContext)).concat(media == null ? void 0 : media.setMediaKeys(null).catch(error => { 23235 this.log(`Could not clear media keys: ${error}`); 23236 }))).then(() => { 23237 if (keySessionCount) { 23238 this.log('finished closing key sessions and clearing media keys'); 23239 mediaKeysList.length = 0; 23240 } 23241 }).catch(error => { 23242 this.log(`Could not close sessions and clear media keys: ${error}`); 23243 }); 23244 } 23245 onManifestLoading() { 23246 this.keyFormatPromise = null; 23247 } 23248 onManifestLoaded(event, { 23249 sessionKeys 23250 }) { 23251 if (!sessionKeys || !this.config.emeEnabled) { 23252 return; 23253 } 23254 if (!this.keyFormatPromise) { 23255 const keyFormats = sessionKeys.reduce((formats, sessionKey) => { 23256 if (formats.indexOf(sessionKey.keyFormat) === -1) { 23257 formats.push(sessionKey.keyFormat); 23258 } 23259 return formats; 23260 }, []); 23261 this.log(`Selecting key-system from session-keys ${keyFormats.join(', ')}`); 23262 this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); 23263 } 23264 } 23265 removeSession(mediaKeySessionContext) { 23266 const { 23267 mediaKeysSession, 23268 licenseXhr 23269 } = mediaKeySessionContext; 23270 if (mediaKeysSession) { 23271 this.log(`Remove licenses and keys and close session ${mediaKeysSession.sessionId}`); 23272 if (mediaKeySessionContext._onmessage) { 23273 mediaKeysSession.removeEventListener('message', mediaKeySessionContext._onmessage); 23274 mediaKeySessionContext._onmessage = undefined; 23275 } 23276 if (mediaKeySessionContext._onkeystatuseschange) { 23277 mediaKeysSession.removeEventListener('keystatuseschange', mediaKeySessionContext._onkeystatuseschange); 23278 mediaKeySessionContext._onkeystatuseschange = undefined; 23279 } 23280 if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) { 23281 licenseXhr.abort(); 23282 } 23283 mediaKeySessionContext.mediaKeysSession = mediaKeySessionContext.decryptdata = mediaKeySessionContext.licenseXhr = undefined; 23284 const index = this.mediaKeySessions.indexOf(mediaKeySessionContext); 23285 if (index > -1) { 23286 this.mediaKeySessions.splice(index, 1); 23287 } 23288 return mediaKeysSession.remove().catch(error => { 23289 this.log(`Could not remove session: ${error}`); 23290 }).then(() => { 23291 return mediaKeysSession.close(); 23292 }).catch(error => { 23293 this.log(`Could not close session: ${error}`); 23294 }); 23295 } 23296 } 23297} 23298EMEController.CDMCleanupPromise = void 0; 23299class EMEKeyError extends Error { 23300 constructor(data, message) { 23301 super(message); 23302 this.data = void 0; 23303 data.error || (data.error = new Error(message)); 23304 this.data = data; 23305 data.err = data.error; 23306 } 23307} 23308 23309/** 23310 * Common Media Object Type 23311 * 23312 * @group CMCD 23313 * @group CMSD 23314 * 23315 * @beta 23316 */ 23317var CmObjectType; 23318(function (CmObjectType) { 23319 /** 23320 * text file, such as a manifest or playlist 23321 */ 23322 CmObjectType["MANIFEST"] = "m"; 23323 /** 23324 * audio only 23325 */ 23326 CmObjectType["AUDIO"] = "a"; 23327 /** 23328 * video only 23329 */ 23330 CmObjectType["VIDEO"] = "v"; 23331 /** 23332 * muxed audio and video 23333 */ 23334 CmObjectType["MUXED"] = "av"; 23335 /** 23336 * init segment 23337 */ 23338 CmObjectType["INIT"] = "i"; 23339 /** 23340 * caption or subtitle 23341 */ 23342 CmObjectType["CAPTION"] = "c"; 23343 /** 23344 * ISOBMFF timed text track 23345 */ 23346 CmObjectType["TIMED_TEXT"] = "tt"; 23347 /** 23348 * cryptographic key, license or certificate. 23349 */ 23350 CmObjectType["KEY"] = "k"; 23351 /** 23352 * other 23353 */ 23354 CmObjectType["OTHER"] = "o"; 23355})(CmObjectType || (CmObjectType = {})); 23356 23357/** 23358 * Common Media Streaming Format 23359 * 23360 * @group CMCD 23361 * @group CMSD 23362 * 23363 * @beta 23364 */ 23365var CmStreamingFormat; 23366(function (CmStreamingFormat) { 23367 /** 23368 * MPEG DASH 23369 */ 23370 CmStreamingFormat["DASH"] = "d"; 23371 /** 23372 * HTTP Live Streaming (HLS) 23373 */ 23374 CmStreamingFormat["HLS"] = "h"; 23375 /** 23376 * Smooth Streaming 23377 */ 23378 CmStreamingFormat["SMOOTH"] = "s"; 23379 /** 23380 * Other 23381 */ 23382 CmStreamingFormat["OTHER"] = "o"; 23383})(CmStreamingFormat || (CmStreamingFormat = {})); 23384 23385/** 23386 * CMCD header fields. 23387 * 23388 * @group CMCD 23389 * 23390 * @beta 23391 */ 23392var CmcdHeaderField; 23393(function (CmcdHeaderField) { 23394 /** 23395 * keys whose values vary with the object being requested. 23396 */ 23397 CmcdHeaderField["OBJECT"] = "CMCD-Object"; 23398 /** 23399 * keys whose values vary with each request. 23400 */ 23401 CmcdHeaderField["REQUEST"] = "CMCD-Request";
23402 /** 23403 * keys whose values are expected to be invariant over the life of the session. 23404 */ 23405 CmcdHeaderField["SESSION"] = "CMCD-Session"; 23406 /** 23407 * keys whose values do not vary with every request or object. 23408 */ 23409 CmcdHeaderField["STATUS"] = "CMCD-Status"; 23410})(CmcdHeaderField || (CmcdHeaderField = {})); 23411 23412/** 23413 * The map of CMCD header fields to official CMCD keys. 23414 * 23415 * @internal 23416 * 23417 * @group CMCD 23418 */ 23419const CmcdHeaderMap = { 23420 [CmcdHeaderField.OBJECT]: ['br', 'd', 'ot', 'tb'], 23421 [CmcdHeaderField.REQUEST]: ['bl', 'dl', 'mtp', 'nor', 'nrr', 'su'], 23422 [CmcdHeaderField.SESSION]: ['cid', 'pr', 'sf', 'sid', 'st', 'v'], 23423 [CmcdHeaderField.STATUS]: ['bs', 'rtp'] 23424}; 23425 23426/** 23427 * Structured Field Item 23428 * 23429 * @group Structured Field 23430 * 23431 * @beta 23432 */ 23433class SfItem { 23434 constructor(value, params) { 23435 this.value = void 0; 23436 this.params = void 0; 23437 if (Array.isArray(value)) { 23438 value = value.map(v => v instanceof SfItem ? v : new SfItem(v)); 23439 } 23440 this.value = value; 23441 this.params = params; 23442 } 23443} 23444 23445/** 23446 * A class to represent structured field tokens when `Symbol` is not available. 23447 * 23448 * @group Structured Field 23449 * 23450 * @beta 23451 */ 23452class SfToken { 23453 constructor(description) { 23454 this.description = void 0; 23455 this.description = description; 23456 } 23457} 23458 23459const DICT = 'Dict'; 23460 23461function format(value) { 23462 if (Array.isArray(value)) { 23463 return JSON.stringify(value); 23464 } 23465 if (value instanceof Map) { 23466 return 'Map{}'; 23467 } 23468 if (value instanceof Set) { 23469 return 'Set{}'; 23470 } 23471 if (typeof value === 'object') { 23472 return JSON.stringify(value); 23473 } 23474 return String(value); 23475} 23476function throwError(action, src, type, cause) { 23477 return new Error(`failed to ${action} "${format(src)}" as ${type}`, { 23478 cause 23479 }); 23480} 23481 23482const BARE_ITEM = 'Bare Item'; 23483 23484const BOOLEAN = 'Boolean'; 23485 23486const BYTES = 'Byte Sequence'; 23487 23488const DECIMAL = 'Decimal'; 23489 23490const INTEGER = 'Integer'; 23491 23492function isInvalidInt(value) { 23493 return value < -999999999999999 || 999999999999999 < value; 23494} 23495 23496const STRING_REGEX = /[\x00-\x1f\x7f]+/; // eslint-disable-line no-control-regex 23497 23498const TOKEN = 'Token'; 23499 23500const KEY = 'Key'; 23501 23502function serializeError(src, type, cause) { 23503 return throwError('serialize', src, type, cause); 23504} 23505 23506// 4.1.9. Serializing a Boolean 23507// 23508// Given a Boolean as input_boolean, return an ASCII string suitable for 23509// use in a HTTP field value. 23510// 23511// 1. If input_boolean is not a boolean, fail serialization. 23512// 23513// 2. Let output be an empty string. 23514// 23515// 3. Append "?" to output. 23516// 23517// 4. If input_boolean is true, append "1" to output. 23518// 23519// 5. If input_boolean is false, append "0" to output. 23520// 23521// 6. Return output. 23522function serializeBoolean(value) { 23523 if (typeof value !== 'boolean') { 23524 throw serializeError(value, BOOLEAN); 23525 } 23526 return value ? '?1' : '?0'; 23527} 23528 23529/** 23530 * Encodes binary data to base64 23531 * 23532 * @param binary - The binary data to encode 23533 * @returns The base64 encoded string 23534 * 23535 * @group Utils 23536 * 23537 * @beta 23538 */ 23539function base64encode(binary) { 23540 return btoa(String.fromCharCode(...binary)); 23541} 23542 23543// 4.1.8. Serializing a Byte Sequence 23544// 23545// Given a Byte Sequence as input_bytes, return an ASCII string suitable 23546// for use in a HTTP field value. 23547// 23548// 1. If input_bytes is not a sequence of bytes, fail serialization. 23549// 23550// 2. Let output be an empty string. 23551// 23552// 3. Append ":" to output. 23553// 23554// 4. Append the result of base64-encoding input_bytes as per 23555// [RFC4648], Section 4, taking account of the requirements below. 23556// 23557// 5. Append ":" to output. 23558// 23559// 6. Return output. 23560// 23561// The encoded data is required to be padded with "=", as per [RFC4648], 23562// Section 3.2. 23563// 23564// Likewise, encoded data SHOULD have pad bits set to zero, as per 23565// [RFC4648], Section 3.5, unless it is not possible to do so due to 23566// implementation constraints. 23567function serializeByteSequence(value) { 23568 if (ArrayBuffer.isView(value) === false) { 23569 throw serializeError(value, BYTES); 23570 } 23571 return `:${base64encode(value)}:`; 23572} 23573 23574// 4.1.4. Serializing an Integer 23575// 23576// Given an Integer as input_integer, return an ASCII string suitable 23577// for use in a HTTP field value. 23578// 23579// 1. If input_integer is not an integer in the range of 23580// -999,999,999,999,999 to 999,999,999,999,999 inclusive, fail 23581// serialization. 23582// 23583// 2. Let output be an empty string. 23584// 23585// 3. If input_integer is less than (but not equal to) 0, append "-" to 23586// output. 23587// 23588// 4. Append input_integer's numeric value represented in base 10 using 23589// only decimal digits to output. 23590// 23591// 5. Return output. 23592function serializeInteger(value) { 23593 if (isInvalidInt(value)) {
vendor: 8,490 bytes, lines 23594-23869
23594 throw serializeError(value, INTEGER); 23595 } 23596 return value.toString(); 23597} 23598 23599// 4.1.10. Serializing a Date 23600// 23601// Given a Date as input_integer, return an ASCII string suitable for 23602// use in an HTTP field value. 23603// 1. Let output be "@". 23604// 2. Append to output the result of running Serializing an Integer 23605// with input_date (Section 4.1.4). 23606// 3. Return output. 23607function serializeDate(value) { 23608 return `@${serializeInteger(value.getTime() / 1000)}`; 23609} 23610 23611/** 23612 * This implements the rounding procedure described in step 2 of the "Serializing a Decimal" specification. 23613 * This rounding style is known as "even rounding", "banker's rounding", or "commercial rounding". 23614 * 23615 * @param value - The value to round 23616 * @param precision - The number of decimal places to round to 23617 * @returns The rounded value 23618 * 23619 * @group Utils 23620 * 23621 * @beta 23622 */ 23623function roundToEven(value, precision) { 23624 if (value < 0) { 23625 return -roundToEven(-value, precision); 23626 } 23627 const decimalShift = Math.pow(10, precision); 23628 const isEquidistant = Math.abs(value * decimalShift % 1 - 0.5) < Number.EPSILON; 23629 if (isEquidistant) { 23630 // If the tail of the decimal place is 'equidistant' we round to the nearest even value 23631 const flooredValue = Math.floor(value * decimalShift); 23632 return (flooredValue % 2 === 0 ? flooredValue : flooredValue + 1) / decimalShift; 23633 } else { 23634 // Otherwise, proceed as normal 23635 return Math.round(value * decimalShift) / decimalShift; 23636 } 23637} 23638 23639// 4.1.5. Serializing a Decimal 23640// 23641// Given a decimal number as input_decimal, return an ASCII string 23642// suitable for use in a HTTP field value. 23643// 23644// 1. If input_decimal is not a decimal number, fail serialization. 23645// 23646// 2. If input_decimal has more than three significant digits to the 23647// right of the decimal point, round it to three decimal places, 23648// rounding the final digit to the nearest value, or to the even 23649// value if it is equidistant. 23650// 23651// 3. If input_decimal has more than 12 significant digits to the left 23652// of the decimal point after rounding, fail serialization. 23653// 23654// 4. Let output be an empty string. 23655// 23656// 5. If input_decimal is less than (but not equal to) 0, append "-" 23657// to output. 23658// 23659// 6. Append input_decimal's integer component represented in base 10 23660// (using only decimal digits) to output; if it is zero, append 23661// "0". 23662// 23663// 7. Append "." to output. 23664// 23665// 8. If input_decimal's fractional component is zero, append "0" to 23666// output. 23667// 23668// 9. Otherwise, append the significant digits of input_decimal's 23669// fractional component represented in base 10 (using only decimal 23670// digits) to output. 23671// 23672// 10. Return output. 23673function serializeDecimal(value) { 23674 const roundedValue = roundToEven(value, 3); // round to 3 decimal places 23675 if (Math.floor(Math.abs(roundedValue)).toString().length > 12) { 23676 throw serializeError(value, DECIMAL); 23677 } 23678 const stringValue = roundedValue.toString(); 23679 return stringValue.includes('.') ? stringValue : `${stringValue}.0`; 23680} 23681 23682const STRING = 'String'; 23683 23684// 4.1.6. Serializing a String 23685// 23686// Given a String as input_string, return an ASCII string suitable for 23687// use in a HTTP field value. 23688// 23689// 1. Convert input_string into a sequence of ASCII characters; if 23690// conversion fails, fail serialization. 23691// 23692// 2. If input_string contains characters in the range %x00-1f or %x7f 23693// (i.e., not in VCHAR or SP), fail serialization. 23694// 23695// 3. Let output be the string DQUOTE. 23696// 23697// 4. For each character char in input_string: 23698// 23699// 1. If char is "\" or DQUOTE: 23700// 23701// 1. Append "\" to output. 23702// 23703// 2. Append char to output. 23704// 23705// 5. Append DQUOTE to output. 23706// 23707// 6. Return output. 23708function serializeString(value) { 23709 if (STRING_REGEX.test(value)) { 23710 throw serializeError(value, STRING); 23711 } 23712 return `"${value.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`)}"`; 23713} 23714 23715function symbolToStr(symbol) { 23716 return symbol.description || symbol.toString().slice(7, -1); 23717} 23718 23719function serializeToken(token) { 23720 const value = symbolToStr(token); 23721 if (/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(value) === false) { 23722 throw serializeError(value, TOKEN); 23723 } 23724 return value; 23725} 23726 23727// 4.1.3.1. Serializing a Bare Item 23728// 23729// Given an Item as input_item, return an ASCII string suitable for use 23730// in a HTTP field value. 23731// 23732// 1. If input_item is an Integer, return the result of running 23733// Serializing an Integer (Section 4.1.4) with input_item. 23734// 23735// 2. If input_item is a Decimal, return the result of running 23736// Serializing a Decimal (Section 4.1.5) with input_item. 23737// 23738// 3. If input_item is a String, return the result of running 23739// Serializing a String (Section 4.1.6) with input_item. 23740// 23741// 4. If input_item is a Token, return the result of running 23742// Serializing a Token (Section 4.1.7) with input_item. 23743// 23744// 5. If input_item is a Boolean, return the result of running 23745// Serializing a Boolean (Section 4.1.9) with input_item. 23746// 23747// 6. If input_item is a Byte Sequence, return the result of running 23748// Serializing a Byte Sequence (Section 4.1.8) with input_item. 23749// 23750// 7. If input_item is a Date, return the result of running Serializing 23751// a Date (Section 4.1.10) with input_item. 23752// 23753// 8. Otherwise, fail serialization. 23754function serializeBareItem(value) { 23755 switch (typeof value) { 23756 case 'number': 23757 if (!isFiniteNumber(value)) { 23758 throw serializeError(value, BARE_ITEM); 23759 } 23760 if (Number.isInteger(value)) { 23761 return serializeInteger(value); 23762 } 23763 return serializeDecimal(value); 23764 case 'string': 23765 return serializeString(value); 23766 case 'symbol': 23767 return serializeToken(value); 23768 case 'boolean': 23769 return serializeBoolean(value); 23770 case 'object': 23771 if (value instanceof Date) { 23772 return serializeDate(value); 23773 } 23774 if (value instanceof Uint8Array) { 23775 return serializeByteSequence(value); 23776 } 23777 if (value instanceof SfToken) { 23778 return serializeToken(value); 23779 } 23780 default: 23781 // fail 23782 throw serializeError(value, BARE_ITEM); 23783 } 23784} 23785 23786// 4.1.1.3. Serializing a Key 23787// 23788// Given a key as input_key, return an ASCII string suitable for use in 23789// a HTTP field value. 23790// 23791// 1. Convert input_key into a sequence of ASCII characters; if 23792// conversion fails, fail serialization. 23793// 23794// 2. If input_key contains characters not in lcalpha, DIGIT, "_", "-", 23795// ".", or "*" fail serialization. 23796// 23797// 3. If the first character of input_key is not lcalpha or "*", fail 23798// serialization. 23799// 23800// 4. Let output be an empty string. 23801// 23802// 5. Append input_key to output. 23803// 23804// 6. Return output. 23805function serializeKey(value) { 23806 if (/^[a-z*][a-z0-9\-_.*]*$/.test(value) === false) { 23807 throw serializeError(value, KEY); 23808 } 23809 return value; 23810} 23811 23812// 4.1.1.2. Serializing Parameters 23813// 23814// Given an ordered Dictionary as input_parameters (each member having a 23815// param_name and a param_value), return an ASCII string suitable for 23816// use in a HTTP field value. 23817// 23818// 1. Let output be an empty string. 23819// 23820// 2. For each param_name with a value of param_value in 23821// input_parameters: 23822// 23823// 1. Append ";" to output. 23824// 23825// 2. Append the result of running Serializing a Key 23826// (Section 4.1.1.3) with param_name to output. 23827// 23828// 3. If param_value is not Boolean true: 23829// 23830// 1. Append "=" to output. 23831// 23832// 2. Append the result of running Serializing a bare Item 23833// (Section 4.1.3.1) with param_value to output. 23834// 23835// 3. Return output. 23836function serializeParams(params) { 23837 if (params == null) { 23838 return ''; 23839 } 23840 return Object.entries(params).map(([key, value]) => { 23841 if (value === true) { 23842 return `;${serializeKey(key)}`; // omit true 23843 } 23844 return `;${serializeKey(key)}=${serializeBareItem(value)}`; 23845 }).join(''); 23846} 23847 23848// 4.1.3. Serializing an Item 23849// 23850// Given an Item as bare_item and Parameters as item_parameters, return 23851// an ASCII string suitable for use in a HTTP field value. 23852// 23853// 1. Let output be an empty string. 23854// 23855// 2. Append the result of running Serializing a Bare Item 23856// Section 4.1.3.1 with bare_item to output. 23857// 23858// 3. Append the result of running Serializing Parameters 23859// Section 4.1.1.2 with item_parameters to output. 23860// 23861// 4. Return output. 23862function serializeItem(value) { 23863 if (value instanceof SfItem) { 23864 return `${serializeBareItem(value.value)}${serializeParams(value.params)}`; 23865 } else { 23866 return serializeBareItem(value); 23867 } 23868} 23869
23870// 4.1.1.1. Serializing an Inner List 23871// 23872// Given an array of (member_value, parameters) tuples as inner_list, 23873// and parameters as list_parameters, return an ASCII string suitable 23874// for use in a HTTP field value. 23875// 23876// 1. Let output be the string "(". 23877// 23878// 2. For each (member_value, parameters) of inner_list: 23879// 23880// 1. Append the result of running Serializing an Item 23881// (Section 4.1.3) with (member_value, parameters) to output. 23882// 23883// 2. If more values remain in inner_list, append a single SP to 23884// output. 23885// 23886// 3. Append ")" to output. 23887// 23888// 4. Append the result of running Serializing Parameters 23889// (Section 4.1.1.2) with list_parameters to output. 23890// 23891// 5. Return output. 23892function serializeInnerList(value) { 23893 return `(${value.value.map(serializeItem).join(' ')})${serializeParams(value.params)}`; 23894} 23895 23896// 4.1.2. Serializing a Dictionary 23897// 23898// Given an ordered Dictionary as input_dictionary (each member having a 23899// member_name and a tuple value of (member_value, parameters)), return 23900// an ASCII string suitable for use in a HTTP field value. 23901// 23902// 1. Let output be an empty string. 23903// 23904// 2. For each member_name with a value of (member_value, parameters) 23905// in input_dictionary: 23906// 23907// 1. Append the result of running Serializing a Key 23908// (Section 4.1.1.3) with member's member_name to output. 23909// 23910// 2. If member_value is Boolean true: 23911// 23912// 1. Append the result of running Serializing Parameters 23913// (Section 4.1.1.2) with parameters to output. 23914// 23915// 3. Otherwise: 23916// 23917// 1. Append "=" to output. 23918// 23919// 2. If member_value is an array, append the result of running 23920// Serializing an Inner List (Section 4.1.1.1) with 23921// (member_value, parameters) to output. 23922// 23923// 3. Otherwise, append the result of running Serializing an 23924// Item (Section 4.1.3) with (member_value, parameters) to 23925// output. 23926// 23927// 4. If more members remain in input_dictionary: 23928// 23929// 1. Append "," to output. 23930// 23931// 2. Append a single SP to output. 23932// 23933// 3. Return output. 23934function serializeDict(dict, options = { 23935 whitespace: true 23936}) { 23937 if (typeof dict !== 'object') { 23938 throw serializeError(dict, DICT); 23939 } 23940 const entries = dict instanceof Map ? dict.entries() : Object.entries(dict); 23941 const optionalWhiteSpace = options != null && options.whitespace ? ' ' : ''; 23942 return Array.from(entries).map(([key, item]) => { 23943 if (item instanceof SfItem === false) { 23944 item = new SfItem(item); 23945 } 23946 let output = serializeKey(key); 23947 if (item.value === true) { 23948 output += serializeParams(item.params); 23949 } else { 23950 output += '='; 23951 if (Array.isArray(item.value)) { 23952 output += serializeInnerList(item); 23953 } else { 23954 output += serializeItem(item); 23955 } 23956 } 23957 return output; 23958 }).join(`,${optionalWhiteSpace}`); 23959} 23960 23961/** 23962 * Encode an object into a structured field dictionary 23963 * 23964 * @param input - The structured field dictionary to encode 23965 * @returns The structured field string 23966 * 23967 * @group Structured Field 23968 * 23969 * @beta 23970 */ 23971function encodeSfDict(value, options) { 23972 return serializeDict(value, options); 23973} 23974 23975/** 23976 * Checks if the given key is a token field. 23977 * 23978 * @param key - The key to check. 23979 * 23980 * @returns `true` if the key is a token field. 23981 * 23982 * @internal 23983 * 23984 * @group CMCD 23985 */ 23986const isTokenField = key => key === 'ot' || key === 'sf' || key === 'st'; 23987 23988const isValid = value => { 23989 if (typeof value === 'number') { 23990 return isFiniteNumber(value); 23991 } 23992 return value != null && value !== '' && value !== false; 23993}; 23994 23995/** 23996 * Constructs a relative path from a URL. 23997 * 23998 * @param url - The destination URL 23999 * @param base - The base URL 24000 * @returns The relative path 24001 * 24002 * @group Utils 24003 * 24004 * @beta 24005 */ 24006function urlToRelativePath(url, base) { 24007 const to = new URL(url); 24008 const from = new URL(base); 24009 if (to.origin !== from.origin) { 24010 return url; 24011 } 24012 const toPath = to.pathname.split('/').slice(1); 24013 const fromPath = from.pathname.split('/').slice(1, -1); 24014 // remove common parents 24015 while (toPath[0] === fromPath[0]) { 24016 toPath.shift(); 24017 fromPath.shift(); 24018 } 24019 // add back paths 24020 while (fromPath.length) { 24021 fromPath.shift(); 24022 toPath.unshift('..'); 24023 } 24024 return toPath.join('/'); 24025} 24026 24027/** 24028 * Generate a random v4 UUID 24029 * 24030 * @returns A random v4 UUID 24031 * 24032 * @group Utils 24033 * 24034 * @beta 24035 */ 24036function uuid() { 24037 try { 24038 return crypto.randomUUID(); 24039 } catch (error) { 24040 try { 24041 const url = URL.createObjectURL(new Blob()); 24042 const uuid = url.toString(); 24043 URL.revokeObjectURL(url); 24044 return uuid.slice(uuid.lastIndexOf('/') + 1); 24045 } catch (error) { 24046 let dt = new Date().getTime(); 24047 const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 24048 const r = (dt + Math.random() * 16) % 16 | 0; 24049 dt = Math.floor(dt / 16); 24050 return (c == 'x' ? r : r & 0x3 | 0x8).toString(16); 24051 }); 24052 return uuid; 24053 } 24054 } 24055} 24056 24057const toRounded = value => Math.round(value); 24058const toUrlSafe = (value, options) => { 24059 if (options != null && options.baseUrl) { 24060 value = urlToRelativePath(value, options.baseUrl); 24061 } 24062 return encodeURIComponent(value); 24063}; 24064const toHundred = value => toRounded(value / 100) * 100; 24065/** 24066 * The default formatters for CMCD values. 24067 * 24068 * @group CMCD 24069 * 24070 * @beta 24071 */ 24072const CmcdFormatters = { 24073 /** 24074 * Bitrate (kbps) rounded integer 24075 */ 24076 br: toRounded, 24077 /** 24078 * Duration (milliseconds) rounded integer 24079 */ 24080 d: toRounded, 24081 /** 24082 * Buffer Length (milliseconds) rounded nearest 100ms 24083 */ 24084 bl: toHundred, 24085 /** 24086 * Deadline (milliseconds) rounded nearest 100ms 24087 */ 24088 dl: toHundred, 24089 /** 24090 * Measured Throughput (kbps) rounded nearest 100kbps 24091 */ 24092 mtp: toHundred, 24093 /** 24094 * Next Object Request URL encoded 24095 */ 24096 nor: toUrlSafe, 24097 /** 24098 * Requested maximum throughput (kbps) rounded nearest 100kbps 24099 */ 24100 rtp: toHundred, 24101 /** 24102 * Top Bitrate (kbps) rounded integer 24103 */ 24104 tb: toRounded 24105}; 24106 24107/** 24108 * Internal CMCD processing function. 24109 * 24110 * @param obj - The CMCD object to process. 24111 * @param map - The mapping function to use. 24112 * @param options - Options for encoding. 24113 * 24114 * @internal 24115 * 24116 * @group CMCD 24117 */ 24118function processCmcd(obj, options) { 24119 const results = {}; 24120 if (obj == null || typeof obj !== 'object') { 24121 return results; 24122 } 24123 const keys = Object.keys(obj).sort(); 24124 const formatters = _extends({}
24124, CmcdFormatters, options == null ? void 0 : options.formatters); 24125 const filter = options == null ? void 0 : options.filter; 24126 keys.forEach(key => { 24127 if (filter != null && filter(key)) { 24128 return; 24129 } 24130 let value = obj[key]; 24131 const formatter = formatters[key]; 24132 if (formatter) { 24133 value = formatter(value, options); 24134 } 24135 // Version should only be reported if not equal to 1. 24136 if (key === 'v' && value === 1) { 24137 return; 24138 } 24139 // Playback rate should only be sent if not equal to 1. 24140 if (key == 'pr' && value === 1) { 24141 return; 24142 } 24143 // ignore invalid values 24144 if (!isValid(value)) { 24145 return; 24146 } 24147 if (isTokenField(key) && typeof value === 'string') { 24148 value = new SfToken(value); 24149 } 24150 results[key] = value; 24151 }); 24152 return results; 24153} 24154 24155/** 24156 * Encode a CMCD object to a string. 24157 * 24158 * @param cmcd - The CMCD object to encode. 24159 * @param options - Options for encoding. 24160 * 24161 * @returns The encoded CMCD string. 24162 * 24163 * @group CMCD 24164 * 24165 * @beta 24166 */ 24167function encodeCmcd(cmcd, options = {}) { 24168 if (!cmcd) { 24169 return ''; 24170 } 24171 return encodeSfDict(processCmcd(cmcd, options), _extends({ 24172 whitespace: false 24173 }, options)); 24174} 24175 24176/** 24177 * Convert a CMCD data object to request headers 24178 * 24179 * @param cmcd - The CMCD data object to convert. 24180 * @param options - Options for encoding the CMCD object. 24181 * 24182 * @returns The CMCD header shards. 24183 * 24184 * @group CMCD 24185 * 24186 * @beta 24187 */ 24188function toCmcdHeaders(cmcd, options = {}) { 24189 if (!cmcd) { 24190 return {}; 24191 } 24192 const entries = Object.entries(cmcd); 24193 const headerMap = Object.entries(CmcdHeaderMap).concat(Object.entries((options == null ? void 0 : options.customHeaderMap) || {})); 24194 const shards = entries.reduce((acc, entry) => { 24195 var _headerMap$find, _acc$field; 24196 const [key, value] = entry; 24197 const field = ((_headerMap$find = headerMap.find(entry => entry[1].includes(key))) == null ? void 0 : _headerMap$find[0]) || CmcdHeaderField.REQUEST; 24198 (_acc$field = acc[field]) != null ? _acc$field : acc[field] = {}; 24199 acc[field][key] = value; 24200 return acc; 24201 }, {}); 24202 return Object.entries(shards).reduce((acc, [field, value]) => { 24203 acc[field] = encodeCmcd(value, options); 24204 return acc; 24205 }, {}); 24206} 24207 24208/** 24209 * Append CMCD query args to a header object. 24210 * 24211 * @param headers - The headers to append to. 24212 * @param cmcd - The CMCD object to append. 24213 * @param customHeaderMap - A map of custom CMCD keys to header fields. 24214 * 24215 * @returns The headers with the CMCD header shards appended. 24216 * 24217 * @group CMCD 24218 * 24219 * @beta 24220 */ 24221function appendCmcdHeaders(headers, cmcd, options) { 24222 return _extends(headers, toCmcdHeaders(cmcd, options)); 24223} 24224 24225/** 24226 * CMCD parameter name. 24227 * 24228 * @group CMCD 24229 * 24230 * @beta 24231 */ 24232const CMCD_PARAM = 'CMCD'; 24233 24234/** 24235 * Convert a CMCD data object to a query arg. 24236 * 24237 * @param cmcd - The CMCD object to convert. 24238 * @param options - Options for encoding the CMCD object. 24239 * 24240 * @returns The CMCD query arg. 24241 * 24242 * @group CMCD 24243 * 24244 * @beta 24245 */ 24246function toCmcdQuery(cmcd, options = {}) { 24247 if (!cmcd) { 24248 return ''; 24249 } 24250 const params = encodeCmcd(cmcd, options); 24251 return `${CMCD_PARAM}=${encodeURIComponent(params)}`; 24252} 24253 24254const REGEX = /CMCD=[^&#]+/; 24255/** 24256 * Append CMCD query args to a URL. 24257 * 24258 * @param url - The URL to append to. 24259 * @param cmcd - The CMCD object to append. 24260 * @param options - Options for encoding the CMCD object. 24261 * 24262 * @returns The URL with the CMCD query args appended. 24263 * 24264 * @group CMCD 24265 * 24266 * @beta 24267 */ 24268function appendCmcdQuery(url, cmcd, options) { 24269 // TODO: Replace with URLSearchParams once we drop Safari < 10.1 & Chrome < 49 support. 24270 // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams 24271 const query = toCmcdQuery(cmcd, options); 24272 if (!query) { 24273 return url; 24274 } 24275 if (REGEX.test(url)) { 24276 return url.replace(REGEX, query); 24277 } 24278 const separator = url.includes('?') ? '&' : '?'; 24279 return `${url}${separator}${query}`; 24280} 24281 24282/** 24283 * Controller to deal with Common Media Client Data (CMCD) 24284 * @see https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf 24285 */ 24286class CMCDController { 24287 // eslint-disable-line no-restricted-globals 24288 24289 constructor(hls) { 24290 this.hls = void 0; 24291 this.config = void 0; 24292 this.media = void 0; 24293 this.sid = void 0; 24294 this.cid = void 0; 24295 this.useHeaders = false;
vendor: 4,149 bytes, lines 24296-24431
24296 this.includeKeys = void 0; 24297 this.initialized = false; 24298 this.starved = false; 24299 this.buffering = true; 24300 this.audioBuffer = void 0; 24301 // eslint-disable-line no-restricted-globals 24302 this.videoBuffer = void 0; 24303 this.onWaiting = () => { 24304 if (this.initialized) { 24305 this.starved = true; 24306 } 24307 this.buffering = true; 24308 }; 24309 this.onPlaying = () => { 24310 if (!this.initialized) { 24311 this.initialized = true; 24312 } 24313 this.buffering = false; 24314 }; 24315 /** 24316 * Apply CMCD data to a manifest request. 24317 */ 24318 this.applyPlaylistData = context => { 24319 try { 24320 this.apply(context, { 24321 ot: CmObjectType.MANIFEST, 24322 su: !this.initialized 24323 }); 24324 } catch (error) { 24325 logger.warn('Could not generate manifest CMCD data.', error); 24326 } 24327 }; 24328 /** 24329 * Apply CMCD data to a segment request 24330 */ 24331 this.applyFragmentData = context => { 24332 try { 24333 const fragment = context.frag; 24334 const level = this.hls.levels[fragment.level]; 24335 const ot = this.getObjectType(fragment); 24336 const data = { 24337 d: fragment.duration * 1000, 24338 ot 24339 }; 24340 if (ot === CmObjectType.VIDEO || ot === CmObjectType.AUDIO || ot == CmObjectType.MUXED) { 24341 data.br = level.bitrate / 1000; 24342 data.tb = this.getTopBandwidth(ot) / 1000; 24343 data.bl = this.getBufferLength(ot); 24344 } 24345 this.apply(context, data); 24346 } catch (error) { 24347 logger.warn('Could not generate segment CMCD data.', error); 24348 } 24349 }; 24350 this.hls = hls; 24351 const config = this.config = hls.config; 24352 const { 24353 cmcd 24354 } = config; 24355 if (cmcd != null) { 24356 config.pLoader = this.createPlaylistLoader(); 24357 config.fLoader = this.createFragmentLoader(); 24358 this.sid = cmcd.sessionId || uuid(); 24359 this.cid = cmcd.contentId; 24360 this.useHeaders = cmcd.useHeaders === true; 24361 this.includeKeys = cmcd.includeKeys; 24362 this.registerListeners(); 24363 } 24364 } 24365 registerListeners() { 24366 const hls = this.hls; 24367 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 24368 hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); 24369 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 24370 } 24371 unregisterListeners() { 24372 const hls = this.hls; 24373 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 24374 hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); 24375 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 24376 } 24377 destroy() { 24378 this.unregisterListeners(); 24379 this.onMediaDetached(); 24380 24381 // @ts-ignore 24382 this.hls = this.config = this.audioBuffer = this.videoBuffer = null; 24383 // @ts-ignore 24384 this.onWaiting = this.onPlaying = null; 24385 } 24386 onMediaAttached(event, data) { 24387 this.media = data.media; 24388 this.media.addEventListener('waiting', this.onWaiting); 24389 this.media.addEventListener('playing', this.onPlaying); 24390 } 24391 onMediaDetached() { 24392 if (!this.media) { 24393 return; 24394 } 24395 this.media.removeEventListener('waiting', this.onWaiting); 24396 this.media.removeEventListener('playing', this.onPlaying); 24397 24398 // @ts-ignore 24399 this.media = null; 24400 } 24401 onBufferCreated(event, data) { 24402 var _data$tracks$audio, _data$tracks$video; 24403 this.audioBuffer = (_data$tracks$audio = data.tracks.audio) == null ? void 0 : _data$tracks$audio.buffer; 24404 this.videoBuffer = (_data$tracks$video = data.tracks.video) == null ? void 0 : _data$tracks$video.buffer; 24405 } 24406 /** 24407 * Create baseline CMCD data 24408 */ 24409 createData() { 24410 var _this$media; 24411 return { 24412 v: 1, 24413 sf: CmStreamingFormat.HLS, 24414 sid: this.sid, 24415 cid: this.cid, 24416 pr: (_this$media = this.media) == null ? void 0 : _this$media.playbackRate, 24417 mtp: this.hls.bandwidthEstimate / 1000 24418 }; 24419 } 24420 24421 /** 24422 * Apply CMCD data to a request. 24423 */ 24424 apply(context, data = {}) { 24425 // apply baseline data 24426 _extends(data, this.createData()); 24427 const isVideo = data.ot === CmObjectType.INIT || data.ot === CmObjectType.VIDEO || data.ot === CmObjectType.MUXED; 24428 if (this.starved && isVideo) { 24429 data.bs = true; 24430 data.su = true; 24431 this.starved = false;
vendor: 5,096 bytes, lines 24432-24638
24432 } 24433 if (data.su == null) { 24434 data.su = this.buffering; 24435 } 24436 24437 // TODO: Implement rtp, nrr, nor, dl 24438 24439 const { 24440 includeKeys 24441 } = this; 24442 if (includeKeys) { 24443 data = Object.keys(data).reduce((acc, key) => { 24444 includeKeys.includes(key) && (acc[key] = data[key]); 24445 return acc; 24446 }, {}); 24447 } 24448 if (this.useHeaders) { 24449 if (!context.headers) { 24450 context.headers = {}; 24451 } 24452 appendCmcdHeaders(context.headers, data); 24453 } else { 24454 context.url = appendCmcdQuery(context.url, data); 24455 } 24456 } 24457 /** 24458 * The CMCD object type. 24459 */ 24460 getObjectType(fragment) { 24461 const { 24462 type 24463 } = fragment; 24464 if (type === 'subtitle') { 24465 return CmObjectType.TIMED_TEXT; 24466 } 24467 if (fragment.sn === 'initSegment') { 24468 return CmObjectType.INIT; 24469 } 24470 if (type === 'audio') { 24471 return CmObjectType.AUDIO; 24472 } 24473 if (type === 'main') { 24474 if (!this.hls.audioTracks.length) { 24475 return CmObjectType.MUXED; 24476 } 24477 return CmObjectType.VIDEO; 24478 } 24479 return undefined; 24480 } 24481 24482 /** 24483 * Get the highest bitrate. 24484 */ 24485 getTopBandwidth(type) { 24486 let bitrate = 0; 24487 let levels; 24488 const hls = this.hls; 24489 if (type === CmObjectType.AUDIO) { 24490 levels = hls.audioTracks; 24491 } else { 24492 const max = hls.maxAutoLevel; 24493 const len = max > -1 ? max + 1 : hls.levels.length; 24494 levels = hls.levels.slice(0, len); 24495 } 24496 for (const level of levels) { 24497 if (level.bitrate > bitrate) { 24498 bitrate = level.bitrate; 24499 } 24500 } 24501 return bitrate > 0 ? bitrate : NaN; 24502 } 24503 24504 /** 24505 * Get the buffer length for a media type in milliseconds 24506 */ 24507 getBufferLength(type) { 24508 const media = this.hls.media; 24509 const buffer = type === CmObjectType.AUDIO ? this.audioBuffer : this.videoBuffer; 24510 if (!buffer || !media) { 24511 return NaN; 24512 } 24513 const info = BufferHelper.bufferInfo(buffer, media.currentTime, this.config.maxBufferHole); 24514 return info.len * 1000; 24515 } 24516 24517 /** 24518 * Create a playlist loader 24519 */ 24520 createPlaylistLoader() { 24521 const { 24522 pLoader 24523 } = this.config; 24524 const apply = this.applyPlaylistData; 24525 const Ctor = pLoader || this.config.loader; 24526 return class CmcdPlaylistLoader { 24527 constructor(config) { 24528 this.loader = void 0; 24529 this.loader = new Ctor(config); 24530 } 24531 get stats() { 24532 return this.loader.stats; 24533 } 24534 get context() { 24535 return this.loader.context; 24536 } 24537 destroy() { 24538 this.loader.destroy(); 24539 } 24540 abort() { 24541 this.loader.abort(); 24542 } 24543 load(context, config, callbacks) { 24544 apply(context); 24545 this.loader.load(context, config, callbacks); 24546 } 24547 }; 24548 } 24549 24550 /** 24551 * Create a playlist loader 24552 */ 24553 createFragmentLoader() { 24554 const { 24555 fLoader 24556 } = this.config; 24557 const apply = this.applyFragmentData; 24558 const Ctor = fLoader || this.config.loader; 24559 return class CmcdFragmentLoader { 24560 constructor(config) { 24561 this.loader = void 0; 24562 this.loader = new Ctor(config); 24563 } 24564 get stats() { 24565 return this.loader.stats; 24566 } 24567 get context() { 24568 return this.loader.context; 24569 } 24570 destroy() { 24571 this.loader.destroy(); 24572 } 24573 abort() { 24574 this.loader.abort(); 24575 } 24576 load(context, config, callbacks) { 24577 apply(context); 24578 this.loader.load(context, config, callbacks); 24579 } 24580 }; 24581 } 24582} 24583 24584const PATHWAY_PENALTY_DURATION_MS = 300000; 24585class ContentSteeringController { 24586 constructor(hls) { 24587 this.hls = void 0; 24588 this.log = void 0; 24589 this.loader = null; 24590 this.uri = null; 24591 this.pathwayId = '.'; 24592 this.pathwayPriority = null; 24593 this.timeToLoad = 300; 24594 this.reloadTimer = -1; 24595 this.updated = 0; 24596 this.started = false; 24597 this.enabled = true; 24598 this.levels = null; 24599 this.audioTracks = null; 24600 this.subtitleTracks = null; 24601 this.penalizedPathways = {}; 24602 this.hls = hls; 24603 this.log = logger.log.bind(logger, `[content-steering]:`); 24604 this.registerListeners(); 24605 } 24606 registerListeners() { 24607 const hls = this.hls; 24608 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 24609 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 24610 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 24611 hls.on(Events.ERROR, this.onError, this); 24612 } 24613 unregisterListeners() { 24614 const hls = this.hls; 24615 if (!hls) { 24616 return; 24617 } 24618 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 24619 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 24620 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 24621 hls.off(Events.ERROR, this.onError, this); 24622 } 24623 startLoad() { 24624 this.started = true; 24625 this.clearTimeout(); 24626 if (this.enabled && this.uri) { 24627 if (this.updated) { 24628 const ttl = this.timeToLoad * 1000 - (performance.now() - this.updated); 24629 if (ttl > 0) { 24630 this.scheduleRefresh(this.uri, ttl); 24631 return; 24632 } 24633 } 24634 this.loadSteeringManifest(this.uri); 24635 } 24636 } 24637 stopLoad() { 24638 this.started = false;
vendor: 4,184 bytes, lines 24639-24761
24639 if (this.loader) { 24640 this.loader.destroy(); 24641 this.loader = null; 24642 } 24643 this.clearTimeout(); 24644 } 24645 clearTimeout() { 24646 if (this.reloadTimer !== -1) { 24647 self.clearTimeout(this.reloadTimer); 24648 this.reloadTimer = -1; 24649 } 24650 } 24651 destroy() { 24652 this.unregisterListeners(); 24653 this.stopLoad(); 24654 // @ts-ignore 24655 this.hls = null; 24656 this.levels = this.audioTracks = this.subtitleTracks = null; 24657 } 24658 removeLevel(levelToRemove) { 24659 const levels = this.levels; 24660 if (levels) { 24661 this.levels = levels.filter(level => level !== levelToRemove); 24662 } 24663 } 24664 onManifestLoading() { 24665 this.stopLoad(); 24666 this.enabled = true; 24667 this.timeToLoad = 300; 24668 this.updated = 0; 24669 this.uri = null; 24670 this.pathwayId = '.'; 24671 this.levels = this.audioTracks = this.subtitleTracks = null; 24672 } 24673 onManifestLoaded(event, data) { 24674 const { 24675 contentSteering 24676 } = data; 24677 if (contentSteering === null) { 24678 return; 24679 } 24680 this.pathwayId = contentSteering.pathwayId; 24681 this.uri = contentSteering.uri; 24682 if (this.started) { 24683 this.startLoad(); 24684 } 24685 } 24686 onManifestParsed(event, data) { 24687 this.audioTracks = data.audioTracks; 24688 this.subtitleTracks = data.subtitleTracks; 24689 } 24690 onError(event, data) { 24691 const { 24692 errorAction 24693 } = data; 24694 if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox && errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost) { 24695 const levels = this.levels; 24696 let pathwayPriority = this.pathwayPriority; 24697 let errorPathway = this.pathwayId; 24698 if (data.context) { 24699 const { 24700 groupId, 24701 pathwayId, 24702 type 24703 } = data.context; 24704 if (groupId && levels) { 24705 errorPathway = this.getPathwayForGroupId(groupId, type, errorPathway); 24706 } else if (pathwayId) { 24707 errorPathway = pathwayId; 24708 } 24709 } 24710 if (!(errorPathway in this.penalizedPathways)) { 24711 this.penalizedPathways[errorPathway] = performance.now(); 24712 } 24713 if (!pathwayPriority && levels) { 24714 // If PATHWAY-PRIORITY was not provided, list pathways for error handling 24715 pathwayPriority = levels.reduce((pathways, level) => { 24716 if (pathways.indexOf(level.pathwayId) === -1) { 24717 pathways.push(level.pathwayId); 24718 } 24719 return pathways; 24720 }, []); 24721 } 24722 if (pathwayPriority && pathwayPriority.length > 1) { 24723 this.updatePathwayPriority(pathwayPriority); 24724 errorAction.resolved = this.pathwayId !== errorPathway; 24725 } 24726 if (!errorAction.resolved) { 24727 logger.warn(`Could not resolve ${data.details} ("${data.error.message}") with content-steering for Pathway: ${errorPathway} levels: ${levels ? levels.length : levels} priorities: ${JSON.stringify(pathwayPriority)} penalized: ${JSON.stringify(this.penalizedPathways)}`); 24728 } 24729 } 24730 } 24731 filterParsedLevels(levels) { 24732 // Filter levels to only include those that are in the initial pathway 24733 this.levels = levels; 24734 let pathwayLevels = this.getLevelsForPathway(this.pathwayId); 24735 if (pathwayLevels.length === 0) { 24736 const pathwayId = levels[0].pathwayId; 24737 this.log(`No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${pathwayId}"`); 24738 pathwayLevels = this.getLevelsForPathway(pathwayId); 24739 this.pathwayId = pathwayId; 24740 } 24741 if (pathwayLevels.length !== levels.length) { 24742 this.log(`Found ${pathwayLevels.length}/${levels.length} levels in Pathway "${this.pathwayId}"`); 24743 return pathwayLevels; 24744 } 24745 return levels; 24746 } 24747 getLevelsForPathway(pathwayId) { 24748 if (this.levels === null) { 24749 return []; 24750 } 24751 return this.levels.filter(level => pathwayId === level.pathwayId); 24752 } 24753 updatePathwayPriority(pathwayPriority) { 24754 this.pathwayPriority = pathwayPriority; 24755 let levels; 24756 24757 // Evaluate if we should remove the pathway from the penalized list 24758 const penalizedPathways = this.penalizedPathways; 24759 const now = performance.now(); 24760 Object.keys(penalizedPathways).forEach(pathwayId => { 24761 if (now - penalizedPathways[pathwayId] > PATHWAY_PENALTY_DURATION_MS) {
vendor: 4,399 bytes, lines 24762-24872
24762 delete penalizedPathways[pathwayId]; 24763 } 24764 }); 24765 for (let i = 0; i < pathwayPriority.length; i++) { 24766 const pathwayId = pathwayPriority[i]; 24767 if (pathwayId in penalizedPathways) { 24768 continue; 24769 } 24770 if (pathwayId === this.pathwayId) { 24771 return; 24772 } 24773 const selectedIndex = this.hls.nextLoadLevel; 24774 const selectedLevel = this.hls.levels[selectedIndex]; 24775 levels = this.getLevelsForPathway(pathwayId); 24776 if (levels.length > 0) { 24777 this.log(`Setting Pathway to "${pathwayId}"`); 24778 this.pathwayId = pathwayId; 24779 reassignFragmentLevelIndexes(levels); 24780 this.hls.trigger(Events.LEVELS_UPDATED, { 24781 levels 24782 }); 24783 // Set LevelController's level to trigger LEVEL_SWITCHING which loads playlist if needed 24784 const levelAfterChange = this.hls.levels[selectedIndex]; 24785 if (selectedLevel && levelAfterChange && this.levels) { 24786 if (levelAfterChange.attrs['STABLE-VARIANT-ID'] !== selectedLevel.attrs['STABLE-VARIANT-ID'] && levelAfterChange.bitrate !== selectedLevel.bitrate) { 24787 this.log(`Unstable Pathways change from bitrate ${selectedLevel.bitrate} to ${levelAfterChange.bitrate}`); 24788 } 24789 this.hls.nextLoadLevel = selectedIndex; 24790 } 24791 break; 24792 } 24793 } 24794 } 24795 getPathwayForGroupId(groupId, type, defaultPathway) { 24796 const levels = this.getLevelsForPathway(defaultPathway).concat(this.levels || []); 24797 for (let i = 0; i < levels.length; i++) { 24798 if (type === PlaylistContextType.AUDIO_TRACK && levels[i].hasAudioGroup(groupId) || type === PlaylistContextType.SUBTITLE_TRACK && levels[i].hasSubtitleGroup(groupId)) { 24799 return levels[i].pathwayId; 24800 } 24801 } 24802 return defaultPathway; 24803 } 24804 clonePathways(pathwayClones) { 24805 const levels = this.levels; 24806 if (!levels) { 24807 return; 24808 } 24809 const audioGroupCloneMap = {}; 24810 const subtitleGroupCloneMap = {}; 24811 pathwayClones.forEach(pathwayClone => { 24812 const { 24813 ID: cloneId, 24814 'BASE-ID': baseId, 24815 'URI-REPLACEMENT': uriReplacement 24816 } = pathwayClone; 24817 if (levels.some(level => level.pathwayId === cloneId)) { 24818 return; 24819 } 24820 const clonedVariants = this.getLevelsForPathway(baseId).map(baseLevel => { 24821 const attributes = new AttrList(baseLevel.attrs); 24822 attributes['PATHWAY-ID'] = cloneId; 24823 const clonedAudioGroupId = attributes.AUDIO && `${attributes.AUDIO}_clone_${cloneId}`; 24824 const clonedSubtitleGroupId = attributes.SUBTITLES && `${attributes.SUBTITLES}_clone_${cloneId}`; 24825 if (clonedAudioGroupId) { 24826 audioGroupCloneMap[attributes.AUDIO] = clonedAudioGroupId; 24827 attributes.AUDIO = clonedAudioGroupId; 24828 } 24829 if (clonedSubtitleGroupId) { 24830 subtitleGroupCloneMap[attributes.SUBTITLES] = clonedSubtitleGroupId; 24831 attributes.SUBTITLES = clonedSubtitleGroupId; 24832 } 24833 const url = performUriReplacement(baseLevel.uri, attributes['STABLE-VARIANT-ID'], 'PER-VARIANT-URIS', uriReplacement); 24834 const clonedLevel = new Level({ 24835 attrs: attributes, 24836 audioCodec: baseLevel.audioCodec, 24837 bitrate: baseLevel.bitrate, 24838 height: baseLevel.height, 24839 name: baseLevel.name, 24840 url, 24841 videoCodec: baseLevel.videoCodec, 24842 width: baseLevel.width 24843 }); 24844 if (baseLevel.audioGroups) { 24845 for (let i = 1; i < baseLevel.audioGroups.length; i++) { 24846 clonedLevel.addGroupId('audio', `${baseLevel.audioGroups[i]}_clone_${cloneId}`); 24847 } 24848 } 24849 if (baseLevel.subtitleGroups) { 24850 for (let i = 1; i < baseLevel.subtitleGroups.length; i++) { 24851 clonedLevel.addGroupId('text', `${baseLevel.subtitleGroups[i]}_clone_${cloneId}`); 24852 } 24853 } 24854 return clonedLevel; 24855 }); 24856 levels.push(...clonedVariants); 24857 cloneRenditionGroups(this.audioTracks, audioGroupCloneMap, uriReplacement, cloneId); 24858 cloneRenditionGroups(this.subtitleTracks, subtitleGroupCloneMap, uriReplacement, cloneId); 24859 }); 24860 } 24861 loadSteeringManifest(uri) { 24862 const config = this.hls.config; 24863 const Loader = config.loader; 24864 if (this.loader) { 24865 this.loader.destroy(); 24866 } 24867 this.loader = new Loader(config); 24868 let url; 24869 try { 24870 url = new self.URL(uri); 24871 } catch (error) { 24872 this.enabled = false;
vendor: 5,045 bytes, lines 24873-25008
24873 this.log(`Failed to parse Steering Manifest URI: ${uri}`); 24874 return; 24875 } 24876 if (url.protocol !== 'data:') { 24877 const throughput = (this.hls.bandwidthEstimate || config.abrEwmaDefaultEstimate) | 0; 24878 url.searchParams.set('_HLS_pathway', this.pathwayId); 24879 url.searchParams.set('_HLS_throughput', '' + throughput); 24880 } 24881 const context = { 24882 responseType: 'json', 24883 url: url.href 24884 }; 24885 const loadPolicy = config.steeringManifestLoadPolicy.default; 24886 const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; 24887 const loaderConfig = { 24888 loadPolicy, 24889 timeout: loadPolicy.maxLoadTimeMs, 24890 maxRetry: legacyRetryCompatibility.maxNumRetry || 0, 24891 retryDelay: legacyRetryCompatibility.retryDelayMs || 0, 24892 maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 24893 }; 24894 const callbacks = { 24895 onSuccess: (response, stats, context, networkDetails) => { 24896 this.log(`Loaded steering manifest: "${url}"`); 24897 const steeringData = response.data; 24898 if (steeringData.VERSION !== 1) { 24899 this.log(`Steering VERSION ${steeringData.VERSION} not supported!`); 24900 return; 24901 } 24902 this.updated = performance.now(); 24903 this.timeToLoad = steeringData.TTL; 24904 const { 24905 'RELOAD-URI': reloadUri, 24906 'PATHWAY-CLONES': pathwayClones, 24907 'PATHWAY-PRIORITY': pathwayPriority 24908 } = steeringData; 24909 if (reloadUri) { 24910 try { 24911 this.uri = new self.URL(reloadUri, url).href; 24912 } catch (error) { 24913 this.enabled = false; 24914 this.log(`Failed to parse Steering Manifest RELOAD-URI: ${reloadUri}`); 24915 return; 24916 } 24917 } 24918 this.scheduleRefresh(this.uri || context.url); 24919 if (pathwayClones) { 24920 this.clonePathways(pathwayClones); 24921 } 24922 const loadedSteeringData = { 24923 steeringManifest: steeringData, 24924 url: url.toString() 24925 }; 24926 this.hls.trigger(Events.STEERING_MANIFEST_LOADED, loadedSteeringData); 24927 if (pathwayPriority) { 24928 this.updatePathwayPriority(pathwayPriority); 24929 } 24930 }, 24931 onError: (error, context, networkDetails, stats) => { 24932 this.log(`Error loading steering manifest: ${error.code} ${error.text} (${context.url})`); 24933 this.stopLoad(); 24934 if (error.code === 410) { 24935 this.enabled = false; 24936 this.log(`Steering manifest ${context.url} no longer available`); 24937 return; 24938 } 24939 let ttl = this.timeToLoad * 1000; 24940 if (error.code === 429) { 24941 const loader = this.loader; 24942 if (typeof (loader == null ? void 0 : loader.getResponseHeader) === 'function') { 24943 const retryAfter = loader.getResponseHeader('Retry-After'); 24944 if (retryAfter) { 24945 ttl = parseFloat(retryAfter) * 1000; 24946 } 24947 } 24948 this.log(`Steering manifest ${context.url} rate limited`); 24949 return; 24950 } 24951 this.scheduleRefresh(this.uri || context.url, ttl); 24952 }, 24953 onTimeout: (stats, context, networkDetails) => { 24954 this.log(`Timeout loading steering manifest (${context.url})`); 24955 this.scheduleRefresh(this.uri || context.url); 24956 } 24957 }; 24958 this.log(`Requesting steering manifest: ${url}`); 24959 this.loader.load(context, loaderConfig, callbacks); 24960 } 24961 scheduleRefresh(uri, ttlMs = this.timeToLoad * 1000) { 24962 this.clearTimeout(); 24963 this.reloadTimer = self.setTimeout(() => { 24964 var _this$hls; 24965 const media = (_this$hls = this.hls) == null ? void 0 : _this$hls.media; 24966 if (media && !media.ended) { 24967 this.loadSteeringManifest(uri); 24968 return; 24969 } 24970 this.scheduleRefresh(uri, this.timeToLoad * 1000); 24971 }, ttlMs); 24972 } 24973} 24974function cloneRenditionGroups(tracks, groupCloneMap, uriReplacement, cloneId) { 24975 if (!tracks) { 24976 return; 24977 } 24978 Object.keys(groupCloneMap).forEach(audioGroupId => { 24979 const clonedTracks = tracks.filter(track => track.groupId === audioGroupId).map(track => { 24980 const clonedTrack = _extends({}, track); 24981 clonedTrack.details = undefined; 24982 clonedTrack.attrs = new AttrList(clonedTrack.attrs); 24983 clonedTrack.url = clonedTrack.attrs.URI = performUriReplacement(track.url, track.attrs['STABLE-RENDITION-ID'], 'PER-RENDITION-URIS', uriReplacement); 24984 clonedTrack.groupId = clonedTrack.attrs['GROUP-ID'] = groupCloneMap[audioGroupId]; 24985 clonedTrack.attrs['PATHWAY-ID'] = cloneId; 24986 return clonedTrack; 24987 }); 24988 tracks.push(...clonedTracks); 24989 }); 24990} 24991function performUriReplacement(uri, stableId, perOptionKey, uriReplacement) { 24992 const { 24993 HOST: host, 24994 PARAMS: params, 24995 [perOptionKey]: perOptionUris 24996 } = uriReplacement; 24997 let perVariantUri; 24998 if (stableId) { 24999 perVariantUri = perOptionUris == null ? void 0 : perOptionUris[stableId]; 25000 if (perVariantUri) { 25001 uri = perVariantUri; 25002 } 25003 } 25004 const url = new self.URL(uri); 25005 if (host && !perVariantUri) { 25006 url.host = host; 25007 } 25008 if (params) {
vendor: 8,897 bytes, lines 25009-25279
25009 Object.keys(params).sort().forEach(key => { 25010 if (key) { 25011 url.searchParams.set(key, params[key]); 25012 } 25013 }); 25014 } 25015 return url.href; 25016} 25017 25018const AGE_HEADER_LINE_REGEX = /^age:\s*[\d.]+\s*$/im; 25019class XhrLoader { 25020 constructor(config) { 25021 this.xhrSetup = void 0; 25022 this.requestTimeout = void 0; 25023 this.retryTimeout = void 0; 25024 this.retryDelay = void 0; 25025 this.config = null; 25026 this.callbacks = null; 25027 this.context = null; 25028 this.loader = null; 25029 this.stats = void 0; 25030 this.xhrSetup = config ? config.xhrSetup || null : null; 25031 this.stats = new LoadStats(); 25032 this.retryDelay = 0; 25033 } 25034 destroy() { 25035 this.callbacks = null; 25036 this.abortInternal(); 25037 this.loader = null; 25038 this.config = null; 25039 this.context = null; 25040 this.xhrSetup = null; 25041 } 25042 abortInternal() { 25043 const loader = this.loader; 25044 self.clearTimeout(this.requestTimeout); 25045 self.clearTimeout(this.retryTimeout); 25046 if (loader) { 25047 loader.onreadystatechange = null; 25048 loader.onprogress = null; 25049 if (loader.readyState !== 4) { 25050 this.stats.aborted = true; 25051 loader.abort(); 25052 } 25053 } 25054 } 25055 abort() { 25056 var _this$callbacks; 25057 this.abortInternal(); 25058 if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { 25059 this.callbacks.onAbort(this.stats, this.context, this.loader); 25060 } 25061 } 25062 load(context, config, callbacks) { 25063 if (this.stats.loading.start) { 25064 throw new Error('Loader can only be used once.'); 25065 } 25066 this.stats.loading.start = self.performance.now(); 25067 this.context = context; 25068 this.config = config; 25069 this.callbacks = callbacks; 25070 this.loadInternal(); 25071 } 25072 loadInternal() { 25073 const { 25074 config, 25075 context 25076 } = this; 25077 if (!config || !context) { 25078 return; 25079 } 25080 const xhr = this.loader = new self.XMLHttpRequest(); 25081 const stats = this.stats; 25082 stats.loading.first = 0; 25083 stats.loaded = 0; 25084 stats.aborted = false; 25085 const xhrSetup = this.xhrSetup; 25086 if (xhrSetup) { 25087 Promise.resolve().then(() => { 25088 if (this.loader !== xhr || this.stats.aborted) return; 25089 return xhrSetup(xhr, context.url); 25090 }).catch(error => { 25091 if (this.loader !== xhr || this.stats.aborted) return; 25092 xhr.open('GET', context.url, true); 25093 return xhrSetup(xhr, context.url); 25094 }).then(() => { 25095 if (this.loader !== xhr || this.stats.aborted) return; 25096 this.openAndSendXhr(xhr, context, config); 25097 }).catch(error => { 25098 // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS 25099 this.callbacks.onError({ 25100 code: xhr.status, 25101 text: error.message 25102 }, context, xhr, stats); 25103 return; 25104 }); 25105 } else { 25106 this.openAndSendXhr(xhr, context, config); 25107 } 25108 } 25109 openAndSendXhr(xhr, context, config) { 25110 if (!xhr.readyState) { 25111 xhr.open('GET', context.url, true); 25112 } 25113 const headers = context.headers; 25114 const { 25115 maxTimeToFirstByteMs, 25116 maxLoadTimeMs 25117 } = config.loadPolicy; 25118 if (headers) { 25119 for (const header in headers) { 25120 xhr.setRequestHeader(header, headers[header]); 25121 } 25122 } 25123 if (context.rangeEnd) { 25124 xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1)); 25125 } 25126 xhr.onreadystatechange = this.readystatechange.bind(this); 25127 xhr.onprogress = this.loadprogress.bind(this); 25128 xhr.responseType = context.responseType; 25129 // setup timeout before we perform request 25130 self.clearTimeout(this.requestTimeout); 25131 config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; 25132 this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.timeout); 25133 xhr.send(); 25134 } 25135 readystatechange() { 25136 const { 25137 context, 25138 loader: xhr, 25139 stats 25140 } = this; 25141 if (!context || !xhr) { 25142 return; 25143 } 25144 const readyState = xhr.readyState; 25145 const config = this.config; 25146 25147 // don't proceed if xhr has been aborted 25148 if (stats.aborted) { 25149 return; 25150 } 25151 25152 // >= HEADERS_RECEIVED 25153 if (readyState >= 2) { 25154 if (stats.loading.first === 0) { 25155 stats.loading.first = Math.max(self.performance.now(), stats.loading.start); 25156 // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet 25157 if (config.timeout !== config.loadPolicy.maxLoadTimeMs) { 25158 self.clearTimeout(this.requestTimeout); 25159 config.timeout = config.loadPolicy.maxLoadTimeMs; 25160 this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.loadPolicy.maxLoadTimeMs - (stats.loading.first - stats.loading.start)); 25161 } 25162 } 25163 if (readyState === 4) { 25164 self.clearTimeout(this.requestTimeout); 25165 xhr.onreadystatechange = null; 25166 xhr.onprogress = null; 25167 const status = xhr.status; 25168 // http status between 200 to 299 are all successful 25169 const useResponse = xhr.responseType !== 'text'; 25170 if (status >= 200 && status < 300 && (useResponse && xhr.response || xhr.responseText !== null)) { 25171 stats.loading.end = Math.max(self.performance.now(), stats.loading.first); 25172 const data = useResponse ? xhr.response : xhr.responseText; 25173 const len = xhr.responseType === 'arraybuffer' ? data.byteLength : data.length; 25174 stats.loaded = stats.total = len; 25175 stats.bwEstimate = stats.total * 8000 / (stats.loading.end - stats.loading.first); 25176 if (!this.callbacks) { 25177 return; 25178 } 25179 const onProgress = this.callbacks.onProgress; 25180 if (onProgress) { 25181 onProgress(stats, context, data, xhr); 25182 } 25183 if (!this.callbacks) { 25184 return; 25185 } 25186 const response = { 25187 url: xhr.responseURL, 25188 data: data, 25189 code: status 25190 }; 25191 this.callbacks.onSuccess(response, stats, context, xhr); 25192 } else { 25193 const retryConfig = config.loadPolicy.errorRetry; 25194 const retryCount = stats.retry; 25195 // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error 25196 const response = { 25197 url: context.url, 25198 data: undefined, 25199 code: status 25200 }; 25201 if (shouldRetry(retryConfig, retryCount, false, response)) { 25202 this.retry(retryConfig); 25203 } else { 25204 logger.error(`${status} while loading ${context.url}`); 25205 this.callbacks.onError({ 25206 code: status, 25207 text: xhr.statusText 25208 }, context, xhr, stats); 25209 } 25210 } 25211 } 25212 } 25213 } 25214 loadtimeout() { 25215 if (!this.config) return; 25216 const retryConfig = this.config.loadPolicy.timeoutRetry; 25217 const retryCount = this.stats.retry; 25218 if (shouldRetry(retryConfig, retryCount, true)) { 25219 this.retry(retryConfig); 25220 } else { 25221 var _this$context; 25222 logger.warn(`timeout while loading ${(_this$context = this.context) == null ? void 0 : _this$context.url}`); 25223 const callbacks = this.callbacks; 25224 if (callbacks) { 25225 this.abortInternal(); 25226 callbacks.onTimeout(this.stats, this.context, this.loader); 25227 } 25228 } 25229 } 25230 retry(retryConfig) { 25231 const { 25232 context, 25233 stats 25234 } = this; 25235 this.retryDelay = getRetryDelay(retryConfig, stats.retry); 25236 stats.retry++; 25237 logger.warn(`${status ? 'HTTP Status ' + status : 'Timeout'} while loading ${context == null ? void 0 : context.url}, retrying ${stats.retry}/${retryConfig.maxNumRetry} in ${this.retryDelay}ms`); 25238 // abort and reset internal state 25239 this.abortInternal(); 25240 this.loader = null; 25241 // schedule retry 25242 self.clearTimeout(this.retryTimeout); 25243 this.retryTimeout = self.setTimeout(this.loadInternal.bind(this), this.retryDelay); 25244 } 25245 loadprogress(event) { 25246 const stats = this.stats; 25247 stats.loaded = event.loaded; 25248 if (event.lengthComputable) { 25249 stats.total = event.total; 25250 } 25251 } 25252 getCacheAge() { 25253 let result = null; 25254 if (this.loader && AGE_HEADER_LINE_REGEX.test(this.loader.getAllResponseHeaders())) { 25255 const ageHeader = this.loader.getResponseHeader('age'); 25256 result = ageHeader ? parseFloat(ageHeader) : null; 25257 } 25258 return result; 25259 } 25260 getResponseHeader(name) { 25261 if (this.loader && new RegExp(`^${name}:\\s*[\\d.]+\\s*$`, 'im').test(this.loader.getAllResponseHeaders())) { 25262 return this.loader.getResponseHeader(name); 25263 } 25264 return null; 25265 } 25266} 25267 25268function fetchSupported() { 25269 if ( 25270 // @ts-ignore 25271 self.fetch && self.AbortController && self.ReadableStream && self.Request) { 25272 try { 25273 new self.ReadableStream({}); // eslint-disable-line no-new 25274 return true; 25275 } catch (e) { 25276 /* noop */ 25277 } 25278 } 25279 return false;
25280} 25281const BYTERANGE = /(\d+)-(\d+)\/(\d+)/; 25282class FetchLoader { 25283 constructor(config /* HlsConfig */) { 25284 this.fetchSetup = void 0; 25285 this.requestTimeout = void 0; 25286 this.request = null; 25287 this.response = null; 25288 this.controller = void 0; 25289 this.context = null; 25290 this.config = null; 25291 this.callbacks = null; 25292 this.stats = void 0; 25293 this.loader = null; 25294 this.fetchSetup = config.fetchSetup || getRequest; 25295 this.controller = new self.AbortController(); 25296 this.stats = new LoadStats(); 25297 } 25298 destroy() { 25299 this.loader = this.callbacks = this.context = this.config = this.request = null; 25300 this.abortInternal(); 25301 this.response = null; 25302 // @ts-ignore 25303 this.fetchSetup = this.controller = this.stats = null; 25304 } 25305 abortInternal() { 25306 if (this.controller && !this.stats.loading.end) { 25307 this.stats.aborted = true; 25308 this.controller.abort(); 25309 } 25310 } 25311 abort() { 25312 var _this$callbacks; 25313 this.abortInternal(); 25314 if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { 25315 this.callbacks.onAbort(this.stats, this.context, this.response); 25316 } 25317 } 25318 load(context, config, callbacks) { 25319 const stats = this.stats; 25320 if (stats.loading.start) { 25321 throw new Error('Loader can only be used once.'); 25322 } 25323 stats.loading.start = self.performance.now(); 25324 const initParams = getRequestParameters(context, this.controller.signal); 25325 const onProgress = callbacks.onProgress; 25326 const isArrayBuffer = context.responseType === 'arraybuffer'; 25327 const LENGTH = isArrayBuffer ? 'byteLength' : 'length'; 25328 const { 25329 maxTimeToFirstByteMs, 25330 maxLoadTimeMs 25331 } = config.loadPolicy; 25332 this.context = context; 25333 this.config = config; 25334 this.callbacks = callbacks; 25335 this.request = this.fetchSetup(context, initParams); 25336 self.clearTimeout(this.requestTimeout); 25337 config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; 25338 this.requestTimeout = self.setTimeout(() => { 25339 this.abortInternal(); 25340 callbacks.onTimeout(stats, context, this.response); 25341 }, config.timeout); 25342 self.fetch(this.request).then(response => { 25343 this.response = this.loader = response; 25344 const first = Math.max(self.performance.now(), stats.loading.start); 25345 self.clearTimeout(this.requestTimeout); 25346 config.timeout = maxLoadTimeMs; 25347 this.requestTimeout = self.setTimeout(() => { 25348 this.abortInternal(); 25349 callbacks.onTimeout(stats, context, this.response); 25350 }, maxLoadTimeMs - (first - stats.loading.start)); 25351 if (!response.ok) { 25352 const { 25353 status, 25354 statusText 25355 } = response; 25356 throw new FetchError(statusText || 'fetch, bad network response', status, response); 25357 } 25358 stats.loading.first = first; 25359 stats.total = getContentLength(response.headers) || stats.total; 25360 if (onProgress && isFiniteNumber(config.highWaterMark)) { 25361 return this.loadProgressively(response, stats, context, config.highWaterMark, onProgress); 25362 } 25363 if (isArrayBuffer) { 25364 return response.arrayBuffer(); 25365 } 25366 if (context.responseType === 'json') { 25367 return response.json(); 25368 } 25369 return response.text(); 25370 }).then(responseData => { 25371 const response = this.response; 25372 if (!response) { 25373 throw new Error('loader destroyed'); 25374 } 25375 self.clearTimeout(this.requestTimeout); 25376 stats.loading.end = Math.max(self.performance.now(), stats.loading.first); 25377 const total = responseData[LENGTH]; 25378 if (total) { 25379 stats.loaded = stats.total = total; 25380 } 25381 const loaderResponse = { 25382 url: response.url, 25383 data: responseData, 25384 code: response.status 25385 }; 25386 if (onProgress && !isFiniteNumber(config.highWaterMark)) { 25387 onProgress(stats, context, responseData, response); 25388 } 25389 callbacks.onSuccess(loaderResponse, stats, context, response); 25390 }).catch(error => { 25391 self.clearTimeout(this.requestTimeout); 25392 if (stats.aborted) { 25393 return; 25394 } 25395 // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior 25396 // when destroying, 'error' itself can be undefined 25397 const code = !error ? 0 : error.code || 0; 25398 const text = !error ? null : error.message; 25399 callbacks.onError({ 25400 code, 25401 text 25402 }, context, error ? error.details : null, stats); 25403 }); 25404 } 25405 getCacheAge() { 25406 let result = null; 25407 if (this.response) { 25408 const ageHeader = this.response.headers.get('age'); 25409 result = ageHeader ? parseFloat(ageHeader) : null; 25410 } 25411 return result; 25412 } 25413 getResponseHeader(name) { 25414 return this.response ? this.response.headers.get(name) : null; 25415 } 25416 loadProgressively(response, stats, context, highWaterMark = 0, onProgress) { 25417 const chunkCache = new ChunkCache(); 25418 const reader = response.body.getReader(); 25419 const pump = () => { 25420 return reader.read().then(data => { 25421 if (data.done) { 25422 if (chunkCache.dataLength) { 25423 onProgress(stats, context, chunkCache.flush(), response); 25424 } 25425 return Promise.resolve(new ArrayBuffer(0)); 25426 } 25427 const chunk = data.value; 25428 const len = chunk.length; 25429 stats.loaded += len; 25430 if (len < highWaterMark || chunkCache.dataLength) { 25431 // The current chunk is too small to to be emitted or the cache already has data 25432 // Push it to the cache 25433 chunkCache.push(chunk); 25434 if (chunkCache.dataLength >= highWaterMark) { 25435 // flush in order to join the typed arrays 25436 onProgress(stats, context, chunkCache.flush(), response); 25437 } 25438 } else { 25439 // If there's nothing cached already, and the chache is large enough 25440 // just emit the progress event 25441 onProgress(stats, context, chunk, response); 25442 } 25443 return pump(); 25444 }).catch(() => { 25445 /* aborted */ 25446 return Promise.reject(); 25447 }); 25448 }; 25449 return pump(); 25450 } 25451} 25452function getRequestParameters(context, signal) { 25453 const initParams = { 25454 method: 'GET', 25455 mode: 'cors', 25456 credentials: 'same-origin', 25457 signal, 25458 headers: new self.Headers(_extends({}, context.headers)) 25459 }; 25460 if (context.rangeEnd) { 25461 initParams.headers.set('Range', 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1)); 25462 } 25463 return initParams; 25464} 25465function getByteRangeLength(byteRangeHeader) { 25466 const result = BYTERANGE.exec(byteRangeHeader); 25467 if (result) { 25468 return parseInt(result[2]) - parseInt(result[1]) + 1; 25469 } 25470} 25471function getContentLength(headers) { 25472 const contentRange = headers.get('Content-Range'); 25473 if (contentRange) { 25474 const byteRangeLength = getByteRangeLength(contentRange); 25475 if (isFiniteNumber(byteRangeLength)) { 25476 return byteRangeLength; 25477 } 25478 } 25479 const contentLength = headers.get('Content-Length'); 25480 if (contentLength) { 25481 return parseInt(contentLength); 25482 } 25483}
vendor: 4,876 bytes, lines 25484-25642
25484function getRequest(context, initParams) { 25485 return new self.Request(context.url, initParams); 25486} 25487class FetchError extends Error { 25488 constructor(message, code, details) { 25489 super(message); 25490 this.code = void 0; 25491 this.details = void 0; 25492 this.code = code; 25493 this.details = details; 25494 } 25495} 25496 25497const WHITESPACE_CHAR = /\s/; 25498const Cues = { 25499 newCue(track, startTime, endTime, captionScreen) { 25500 const result = []; 25501 let row; 25502 // the type data states this is VTTCue, but it can potentially be a TextTrackCue on old browsers 25503 let cue; 25504 let indenting; 25505 let indent; 25506 let text; 25507 const Cue = self.VTTCue || self.TextTrackCue; 25508 for (let r = 0; r < captionScreen.rows.length; r++) { 25509 row = captionScreen.rows[r]; 25510 indenting = true; 25511 indent = 0; 25512 text = ''; 25513 if (!row.isEmpty()) { 25514 var _track$cues; 25515 for (let c = 0; c < row.chars.length; c++) { 25516 if (WHITESPACE_CHAR.test(row.chars[c].uchar) && indenting) { 25517 indent++; 25518 } else { 25519 text += row.chars[c].uchar; 25520 indenting = false; 25521 } 25522 } 25523 // To be used for cleaning-up orphaned roll-up captions 25524 row.cueStartTime = startTime; 25525 25526 // Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE 25527 if (startTime === endTime) { 25528 endTime += 0.0001; 25529 } 25530 if (indent >= 16) { 25531 indent--; 25532 } else { 25533 indent++; 25534 } 25535 const cueText = fixLineBreaks(text.trim()); 25536 const id = generateCueId(startTime, endTime, cueText); 25537 25538 // If this cue already exists in the track do not push it 25539 if (!(track != null && (_track$cues = track.cues) != null && _track$cues.getCueById(id))) { 25540 cue = new Cue(startTime, endTime, cueText); 25541 cue.id = id; 25542 cue.line = r + 1; 25543 cue.align = 'left'; 25544 // Clamp the position between 10 and 80 percent (CEA-608 PAC indent code) 25545 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 25546 // Firefox throws an exception and captions break with out of bounds 0-100 values 25547 cue.position = 10 + Math.min(80, Math.floor(indent * 8 / 32) * 10); 25548 result.push(cue); 25549 } 25550 } 25551 } 25552 if (track && result.length) { 25553 // Sort bottom cues in reverse order so that they render in line order when overlapping in Chrome 25554 result.sort((cueA, cueB) => { 25555 if (cueA.line === 'auto' || cueB.line === 'auto') { 25556 return 0; 25557 } 25558 if (cueA.line > 8 && cueB.line > 8) { 25559 return cueB.line - cueA.line; 25560 } 25561 return cueA.line - cueB.line; 25562 }); 25563 result.forEach(cue => addCueToTrack(track, cue)); 25564 } 25565 return result; 25566 } 25567}; 25568 25569/** 25570 * @deprecated use fragLoadPolicy.default 25571 */ 25572 25573/** 25574 * @deprecated use manifestLoadPolicy.default and playlistLoadPolicy.default 25575 */ 25576 25577const defaultLoadPolicy = { 25578 maxTimeToFirstByteMs: 8000, 25579 maxLoadTimeMs: 20000, 25580 timeoutRetry: null, 25581 errorRetry: null 25582}; 25583 25584/** 25585 * @ignore 25586 * If possible, keep hlsDefaultConfig shallow 25587 * It is cloned whenever a new Hls instance is created, by keeping the config 25588 * shallow the properties are cloned, and we don't end up manipulating the default 25589 */ 25590const hlsDefaultConfig = _objectSpread2(_objectSpread2({ 25591 autoStartLoad: true, 25592 // used by stream-controller 25593 startPosition: -1, 25594 // used by stream-controller 25595 defaultAudioCodec: undefined, 25596 // used by stream-controller 25597 debug: false, 25598 // used by logger 25599 capLevelOnFPSDrop: false, 25600 // used by fps-controller 25601 capLevelToPlayerSize: false, 25602 // used by cap-level-controller 25603 ignoreDevicePixelRatio: false, 25604 // used by cap-level-controller 25605 preferManagedMediaSource: true, 25606 initialLiveManifestSize: 1, 25607 // used by stream-controller 25608 maxBufferLength: 30, 25609 // used by stream-controller 25610 backBufferLength: Infinity, 25611 // used by buffer-controller 25612 frontBufferFlushThreshold: Infinity, 25613 maxBufferSize: 60 * 1000 * 1000, 25614 // used by stream-controller 25615 maxBufferHole: 0.1, 25616 // used by stream-controller 25617 highBufferWatchdogPeriod: 2, 25618 // used by stream-controller 25619 nudgeOffset: 0.1, 25620 // used by stream-controller 25621 nudgeMaxRetry: 3, 25622 // used by stream-controller 25623 maxFragLookUpTolerance: 0.25, 25624 // used by stream-controller 25625 liveSyncDurationCount: 3, 25626 // used by latency-controller 25627 liveMaxLatencyDurationCount: Infinity, 25628 // used by latency-controller 25629 liveSyncDuration: undefined, 25630 // used by latency-controller 25631 liveMaxLatencyDuration: undefined, 25632 // used by latency-controller 25633 maxLiveSyncPlaybackRate: 1, 25634 // used by latency-controller 25635 liveDurationInfinity: false, 25636 // used by buffer-controller 25637 /** 25638 * @deprecated use backBufferLength 25639 */ 25640 liveBackBufferLength: null, 25641 // used by buffer-controller 25642 maxMaxBufferLength: 600,
25643 // used by stream-controller 25644 enableWorker: true, 25645 // used by transmuxer 25646 workerPath: null, 25647 // used by transmuxer 25648 enableSoftwareAES: true, 25649 // used by decrypter 25650 startLevel: undefined, 25651 // used by level-controller 25652 startFragPrefetch: false, 25653 // used by stream-controller 25654 fpsDroppedMonitoringPeriod: 5000, 25655 // used by fps-controller 25656 fpsDroppedMonitoringThreshold: 0.2, 25657 // used by fps-controller 25658 appendErrorMaxRetry: 3, 25659 // used by buffer-controller 25660 loader: XhrLoader, 25661 // loader: FetchLoader, 25662 fLoader: undefined, 25663 // used by fragment-loader 25664 pLoader: undefined, 25665 // used by playlist-loader 25666 xhrSetup: undefined, 25667 // used by xhr-loader 25668 licenseXhrSetup: undefined, 25669 // used by eme-controller 25670 licenseResponseCallback: undefined, 25671 // used by eme-controller 25672 abrController: AbrController, 25673 bufferController: BufferController, 25674 capLevelController: CapLevelController, 25675 errorController: ErrorController, 25676 fpsController: FPSController, 25677 stretchShortVideoTrack: false, 25678 // used by mp4-remuxer 25679 maxAudioFramesDrift: 1, 25680 // used by mp4-remuxer 25681 forceKeyFrameOnDiscontinuity: true, 25682 // used by ts-demuxer 25683 abrEwmaFastLive: 3, 25684 // used by abr-controller 25685 abrEwmaSlowLive: 9, 25686 // used by abr-controller 25687 abrEwmaFastVoD: 3, 25688 // used by abr-controller 25689 abrEwmaSlowVoD: 9, 25690 // used by abr-controller 25691 abrEwmaDefaultEstimate: 5e5, 25692 // 500 kbps // used by abr-controller 25693 abrEwmaDefaultEstimateMax: 5e6, 25694 // 5 mbps 25695 abrBandWidthFactor: 0.95, 25696 // used by abr-controller 25697 abrBandWidthUpFactor: 0.7, 25698 // used by abr-controller 25699 abrMaxWithRealBitrate: false, 25700 // used by abr-controller 25701 maxStarvationDelay: 4, 25702 // used by abr-controller 25703 maxLoadingDelay: 4, 25704 // used by abr-controller 25705 minAutoBitrate: 0, 25706 // used by hls 25707 emeEnabled: false, 25708 // used by eme-controller 25709 widevineLicenseUrl: undefined, 25710 // used by eme-controller 25711 drmSystems: {}, 25712 // used by eme-controller 25713 drmSystemOptions: {}, 25714 // used by eme-controller 25715 requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess , 25716 // used by eme-controller 25717 testBandwidth: true, 25718 progressive: false, 25719 lowLatencyMode: true, 25720 cmcd: undefined, 25721 enableDateRangeMetadataCues: true, 25722 enableEmsgMetadataCues: true, 25723 enableID3MetadataCues: true, 25724 useMediaCapabilities: true, 25725 certLoadPolicy: { 25726 default: defaultLoadPolicy 25727 }, 25728 keyLoadPolicy: { 25729 default: { 25730 maxTimeToFirstByteMs: 8000, 25731 maxLoadTimeMs: 20000, 25732 timeoutRetry: { 25733 maxNumRetry: 1, 25734 retryDelayMs: 1000, 25735 maxRetryDelayMs: 20000, 25736 backoff: 'linear' 25737 }, 25738 errorRetry: { 25739 maxNumRetry: 8, 25740 retryDelayMs: 1000, 25741 maxRetryDelayMs: 20000, 25742 backoff: 'linear' 25743 } 25744 } 25745 }, 25746 manifestLoadPolicy: { 25747 default: { 25748 maxTimeToFirstByteMs: Infinity, 25749 maxLoadTimeMs: 20000, 25750 timeoutRetry: { 25751 maxNumRetry: 2, 25752 retryDelayMs: 0, 25753 maxRetryDelayMs: 0 25754 }, 25755 errorRetry: { 25756 maxNumRetry: 1, 25757 retryDelayMs: 1000, 25758 maxRetryDelayMs: 8000 25759 } 25760 } 25761 }, 25762 playlistLoadPolicy: { 25763 default: { 25764 maxTimeToFirstByteMs: 10000, 25765 maxLoadTimeMs: 20000, 25766 timeoutRetry: { 25767 maxNumRetry: 2, 25768 retryDelayMs: 0, 25769 maxRetryDelayMs: 0 25770 }, 25771 errorRetry: { 25772 maxNumRetry: 2, 25773 retryDelayMs: 1000, 25774 maxRetryDelayMs: 8000 25775 } 25776 } 25777 }, 25778 fragLoadPolicy: { 25779 default: { 25780 maxTimeToFirstByteMs: 10000, 25781 maxLoadTimeMs: 120000, 25782 timeoutRetry: { 25783 maxNumRetry: 4, 25784 retryDelayMs: 0, 25785 maxRetryDelayMs: 0 25786 }, 25787 errorRetry: { 25788 maxNumRetry: 6, 25789 retryDelayMs: 1000, 25790 maxRetryDelayMs: 8000 25791 } 25792 } 25793 }, 25794 steeringManifestLoadPolicy: { 25795 default: { 25796 maxTimeToFirstByteMs: 10000, 25797 maxLoadTimeMs: 20000, 25798 timeoutRetry: { 25799 maxNumRetry: 2, 25800 retryDelayMs: 0, 25801 maxRetryDelayMs: 0 25802 }, 25803 errorRetry: { 25804 maxNumRetry: 1, 25805 retryDelayMs: 1000, 25806 maxRetryDelayMs: 8000 25807 } 25808 } 25809 },
vendor: 4,542 bytes, lines 25810-25918
25810 // These default settings are deprecated in favor of the above policies 25811 // and are maintained for backwards compatibility 25812 manifestLoadingTimeOut: 10000, 25813 manifestLoadingMaxRetry: 1, 25814 manifestLoadingRetryDelay: 1000, 25815 manifestLoadingMaxRetryTimeout: 64000, 25816 levelLoadingTimeOut: 10000, 25817 levelLoadingMaxRetry: 4, 25818 levelLoadingRetryDelay: 1000, 25819 levelLoadingMaxRetryTimeout: 64000, 25820 fragLoadingTimeOut: 20000, 25821 fragLoadingMaxRetry: 6, 25822 fragLoadingRetryDelay: 1000, 25823 fragLoadingMaxRetryTimeout: 64000 25824}, timelineConfig()), {}, { 25825 subtitleStreamController: SubtitleStreamController , 25826 subtitleTrackController: SubtitleTrackController , 25827 timelineController: TimelineController , 25828 audioStreamController: AudioStreamController , 25829 audioTrackController: AudioTrackController , 25830 emeController: EMEController , 25831 cmcdController: CMCDController , 25832 contentSteeringController: ContentSteeringController 25833}); 25834function timelineConfig() { 25835 return { 25836 cueHandler: Cues, 25837 // used by timeline-controller 25838 enableWebVTT: true, 25839 // used by timeline-controller 25840 enableIMSC1: true, 25841 // used by timeline-controller 25842 enableCEA708Captions: true, 25843 // used by timeline-controller 25844 captionsTextTrack1Label: 'English', 25845 // used by timeline-controller 25846 captionsTextTrack1LanguageCode: 'en', 25847 // used by timeline-controller 25848 captionsTextTrack2Label: 'Spanish', 25849 // used by timeline-controller 25850 captionsTextTrack2LanguageCode: 'es', 25851 // used by timeline-controller 25852 captionsTextTrack3Label: 'Unknown CC', 25853 // used by timeline-controller 25854 captionsTextTrack3LanguageCode: '', 25855 // used by timeline-controller 25856 captionsTextTrack4Label: 'Unknown CC', 25857 // used by timeline-controller 25858 captionsTextTrack4LanguageCode: '', 25859 // used by timeline-controller 25860 renderTextTracksNatively: true 25861 }; 25862} 25863 25864/** 25865 * @ignore 25866 */ 25867function mergeConfig(defaultConfig, userConfig) { 25868 if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) { 25869 throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration"); 25870 } 25871 if (userConfig.liveMaxLatencyDurationCount !== undefined && (userConfig.liveSyncDurationCount === undefined || userConfig.liveMaxLatencyDurationCount <= userConfig.liveSyncDurationCount)) { 25872 throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"'); 25873 } 25874 if (userConfig.liveMaxLatencyDuration !== undefined && (userConfig.liveSyncDuration === undefined || userConfig.liveMaxLatencyDuration <= userConfig.liveSyncDuration)) { 25875 throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"'); 25876 } 25877 const defaultsCopy = deepCpy(defaultConfig); 25878 25879 // Backwards compatibility with deprecated config values 25880 const deprecatedSettingTypes = ['manifest', 'level', 'frag']; 25881 const deprecatedSettings = ['TimeOut', 'MaxRetry', 'RetryDelay', 'MaxRetryTimeout']; 25882 deprecatedSettingTypes.forEach(type => { 25883 const policyName = `${type === 'level' ? 'playlist' : type}LoadPolicy`; 25884 const policyNotSet = userConfig[policyName] === undefined; 25885 const report = []; 25886 deprecatedSettings.forEach(setting => { 25887 const deprecatedSetting = `${type}Loading${setting}`; 25888 const value = userConfig[deprecatedSetting]; 25889 if (value !== undefined && policyNotSet) { 25890 report.push(deprecatedSetting); 25891 const settings = defaultsCopy[policyName].default; 25892 userConfig[policyName] = { 25893 default: settings 25894 }; 25895 switch (setting) { 25896 case 'TimeOut': 25897 settings.maxLoadTimeMs = value; 25898 settings.maxTimeToFirstByteMs = value; 25899 break; 25900 case 'MaxRetry': 25901 settings.errorRetry.maxNumRetry = value; 25902 settings.timeoutRetry.maxNumRetry = value; 25903 break; 25904 case 'RetryDelay': 25905 settings.errorRetry.retryDelayMs = value; 25906 settings.timeoutRetry.retryDelayMs = value; 25907 break; 25908 case 'MaxRetryTimeout': 25909 settings.errorRetry.maxRetryDelayMs = value; 25910 settings.timeoutRetry.maxRetryDelayMs = value; 25911 break; 25912 } 25913 } 25914 }); 25915 if (report.length) { 25916 logger.warn(`hls.js config: "${report.join('", "')}" setting(s) are deprecated, use "${policyName}": ${JSON.stringify(userConfig[policyName])}`); 25917 } 25918 });
25919 return _objectSpread2(_objectSpread2({}, defaultsCopy), userConfig); 25920} 25921function deepCpy(obj) { 25922 if (obj && typeof obj === 'object') { 25923 if (Array.isArray(obj)) { 25924 return obj.map(deepCpy); 25925 } 25926 return Object.keys(obj).reduce((result, key) => { 25927 result[key] = deepCpy(obj[key]); 25928 return result; 25929 }, {}); 25930 } 25931 return obj; 25932} 25933 25934/** 25935 * @ignore 25936 */ 25937function enableStreamingMode(config) { 25938 const currentLoader = config.loader; 25939 if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) { 25940 // If a developer has configured their own loader, respect that choice 25941 logger.log('[config]: Custom loader detected, cannot enable progressive streaming'); 25942 config.progressive = false; 25943 } else { 25944 const canStreamProgressively = fetchSupported(); 25945 if (canStreamProgressively) { 25946 config.loader = FetchLoader; 25947 config.progressive = true; 25948 config.enableSoftwareAES = true; 25949 logger.log('[config]: Progressive streaming enabled, using FetchLoader'); 25950 } 25951 } 25952} 25953 25954let chromeOrFirefox; 25955class LevelController extends BasePlaylistController { 25956 constructor(hls, contentSteeringController) { 25957 super(hls, '[level-controller]'); 25958 this._levels = []; 25959 this._firstLevel = -1; 25960 this._maxAutoLevel = -1; 25961 this._startLevel = void 0; 25962 this.currentLevel = null; 25963 this.currentLevelIndex = -1; 25964 this.manualLevelIndex = -1; 25965 this.steering = void 0; 25966 this.onParsedComplete = void 0; 25967 this.steering = contentSteeringController; 25968 this._registerListeners(); 25969 } 25970 _registerListeners() { 25971 const { 25972 hls 25973 } = this; 25974 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 25975 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 25976 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 25977 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 25978 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 25979 hls.on(Events.ERROR, this.onError, this); 25980 } 25981 _unregisterListeners() { 25982 const { 25983 hls 25984 } = this; 25985 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 25986 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 25987 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 25988 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 25989 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 25990 hls.off(Events.ERROR, this.onError, this); 25991 } 25992 destroy() { 25993 this._unregisterListeners(); 25994 this.steering = null; 25995 this.resetLevels(); 25996 super.destroy(); 25997 } 25998 stopLoad() { 25999 const levels = this._levels; 26000 26001 // clean up live level details to force reload them, and reset load errors 26002 levels.forEach(level => { 26003 level.loadError = 0; 26004 level.fragmentError = 0; 26005 }); 26006 super.stopLoad(); 26007 } 26008 resetLevels() { 26009 this._startLevel = undefined; 26010 this.manualLevelIndex = -1; 26011 this.currentLevelIndex = -1; 26012 this.currentLevel = null; 26013 this._levels = []; 26014 this._maxAutoLevel = -1; 26015 } 26016 onManifestLoading(event, data) { 26017 this.resetLevels(); 26018 } 26019 onManifestLoaded(event, data) { 26020 const preferManagedMediaSource = this.hls.config.preferManagedMediaSource; 26021 const levels = []; 26022 const redundantSet = {}; 26023 const generatePathwaySet = {}; 26024 let resolutionFound = false; 26025 let videoCodecFound = false; 26026 let audioCodecFound = false; 26027 data.levels.forEach(levelParsed => { 26028 var _audioCodec, _videoCodec; 26029 const attributes = levelParsed.attrs; 26030 26031 // erase audio codec info if browser does not support mp4a.40.34. 26032 // demuxer will autodetect codec and fallback to mpeg/audio 26033 let { 26034 audioCodec, 26035 videoCodec 26036 } = levelParsed; 26037 if (((_audioCodec = audioCodec) == null ? void 0 : _audioCodec.indexOf('mp4a.40.34')) !== -1) { 26038 chromeOrFirefox || (chromeOrFirefox = /chrome|firefox/i.test(navigator.userAgent)); 26039 if (chromeOrFirefox) { 26040 levelParsed.audioCodec = audioCodec = undefined; 26041 } 26042 } 26043 if (audioCodec) { 26044 levelParsed.audioCodec = audioCodec = getCodecCompatibleName(audioCodec, preferManagedMediaSource); 26045 } 26046 if (((_videoCodec = videoCodec) == null ? void 0 : _videoCodec.indexOf('avc1')) === 0) { 26047 videoCodec = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec); 26048 } 26049 26050 // only keep levels with supported audio/video codecs 26051 const { 26052 width, 26053 height, 26054 unknownCodecs 26055 } = levelParsed; 26056 resolutionFound || (resolutionFound = !!(width && height)); 26057 videoCodecFound || (videoCodecFound = !!videoCodec); 26058 audioCodecFound || (audioCodecFound = !!audioCodec); 26059 if (unknownCodecs != null && unknownCodecs.length || audioCodec && !areCodecsMediaSourceSupp
26059orted(audioCodec, 'audio', preferManagedMediaSource) || videoCodec && !areCodecsMediaSourceSupported(videoCodec, 'video', preferManagedMediaSource)) { 26060 return; 26061 } 26062 const { 26063 CODECS, 26064 'FRAME-RATE': FRAMERATE, 26065 'HDCP-LEVEL': HDCP, 26066 'PATHWAY-ID': PATHWAY, 26067 RESOLUTION, 26068 'VIDEO-RANGE': VIDEO_RANGE 26069 } = attributes; 26070 const contentSteeringPrefix = `${PATHWAY || '.'}-`; 26071 const levelKey = `${contentSteeringPrefix}${levelParsed.bitrate}-${RESOLUTION}-${FRAMERATE}-${CODECS}-${VIDEO_RANGE}-${HDCP}`; 26072 if (!redundantSet[levelKey]) { 26073 const level = new Level(levelParsed); 26074 redundantSet[levelKey] = level; 26075 generatePathwaySet[levelKey] = 1; 26076 levels.push(level); 26077 } else if (redundantSet[levelKey].uri !== levelParsed.url && !levelParsed.attrs['PATHWAY-ID']) { 26078 // Assign Pathway IDs to Redundant Streams (default Pathways is ".". Redundant Streams "..", "...", and so on.) 26079 // Content Steering controller to handles Pathway fallback on error 26080 const pathwayCount = generatePathwaySet[levelKey] += 1; 26081 levelParsed.attrs['PATHWAY-ID'] = new Array(pathwayCount + 1).join('.'); 26082 const level = new Level(levelParsed); 26083 redundantSet[levelKey] = level; 26084 levels.push(level); 26085 } else { 26086 redundantSet[levelKey].addGroupId('audio', attributes.AUDIO); 26087 redundantSet[levelKey].addGroupId('text', attributes.SUBTITLES); 26088 } 26089 }); 26090 this.filterAndSortMediaOptions(levels, data, resolutionFound, videoCodecFound, audioCodecFound); 26091 } 26092 filterAndSortMediaOptions(filteredLevels, data, resolutionFound, videoCodecFound, audioCodecFound) { 26093 let audioTracks = []; 26094 let subtitleTracks = []; 26095 let levels = filteredLevels; 26096 26097 // remove audio-only and invalid video-range levels if we also have levels with video codecs or RESOLUTION signalled 26098 if ((resolutionFound || videoCodecFound) && audioCodecFound) { 26099 levels = levels.filter(({ 26100 videoCodec, 26101 videoRange, 26102 width, 26103 height 26104 }) => (!!videoCodec || !!(width && height)) && isVideoRange(videoRange)); 26105 } 26106 if (levels.length === 0) { 26107 // Dispatch error after MANIFEST_LOADED is done propagating 26108 Promise.resolve().then(() => { 26109 if (this.hls) { 26110 if (data.levels.length) { 26111 this.warn(`One or more CODECS in variant not supported: ${JSON.stringify(data.levels[0].attrs)}`); 26112 } 26113 const error = new Error('no level with compatible codecs found in manifest'); 26114 this.hls.trigger(Events.ERROR, { 26115 type: ErrorTypes.MEDIA_ERROR, 26116 details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, 26117 fatal: true, 26118 url: data.url, 26119 error, 26120 reason: error.message 26121 }); 26122 } 26123 }); 26124 return; 26125 } 26126 if (data.audioTracks) { 26127 const { 26128 preferManagedMediaSource 26129 } = this.hls.config; 26130 audioTracks = data.audioTracks.filter(track => !track.audioCodec || areCodecsMediaSourceSupported(track.audioCodec, 'audio', preferManagedMediaSource)); 26131 // Assign ids after filtering as array indices by group-id 26132 assignTrackIdsByGroup(audioTracks); 26133 } 26134 if (data.subtitles) { 26135 subtitleTracks = data.subtitles; 26136 assignTrackIdsByGroup(subtitleTracks); 26137 } 26138 // start bitrate is the first bitrate of the manifest 26139 const unsortedLevels = levels.slice(0); 26140 // sort levels from lowest to highest 26141 levels.sort((a, b) => { 26142 if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) { 26143 return (a.attrs['HDCP-LEVEL'] || '') > (b.attrs['HDCP-LEVEL'] || '') ? 1 : -1; 26144 } 26145 // sort on height before bitrate for cap-level-controller 26146 if (resolutionFound && a.height !== b.height) { 26147 return a.height - b.height; 26148 } 26149 if (a.frameRate !== b.frameRate) { 26150 return a.frameRate - b.frameRate; 26151 } 26152 if (a.videoRange !== b.videoRange) { 26153 return VideoRangeValues.indexOf(a.videoRange) - VideoRangeValues.indexOf(b.videoRange); 26154 } 26155 if (a.videoCodec !== b.videoCodec) { 26156 const valueA = videoCodecPreferenceValue(a.videoCodec); 26157 const valueB = videoCodecPreferenceValue(b.videoCodec); 26158 if (valueA !== valueB) { 26159 return valueB - valueA; 26160 } 26161 }
vendor: 10,167 bytes, lines 26162-26446
26162 if (a.uri === b.uri && a.codecSet !== b.codecSet) { 26163 const valueA = codecsSetSelectionPreferenceValue(a.codecSet); 26164 const valueB = codecsSetSelectionPreferenceValue(b.codecSet); 26165 if (valueA !== valueB) { 26166 return valueB - valueA; 26167 } 26168 } 26169 if (a.averageBitrate !== b.averageBitrate) { 26170 return a.averageBitrate - b.averageBitrate; 26171 } 26172 return 0; 26173 }); 26174 let firstLevelInPlaylist = unsortedLevels[0]; 26175 if (this.steering) { 26176 levels = this.steering.filterParsedLevels(levels); 26177 if (levels.length !== unsortedLevels.length) { 26178 for (let i = 0; i < unsortedLevels.length; i++) { 26179 if (unsortedLevels[i].pathwayId === levels[0].pathwayId) { 26180 firstLevelInPlaylist = unsortedLevels[i]; 26181 break; 26182 } 26183 } 26184 } 26185 } 26186 this._levels = levels; 26187 26188 // find index of first level in sorted levels 26189 for (let i = 0; i < levels.length; i++) { 26190 if (levels[i] === firstLevelInPlaylist) { 26191 var _this$hls$userConfig; 26192 this._firstLevel = i; 26193 const firstLevelBitrate = firstLevelInPlaylist.bitrate; 26194 const bandwidthEstimate = this.hls.bandwidthEstimate; 26195 this.log(`manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelBitrate}`); 26196 // Update default bwe to first variant bitrate as long it has not been configured or set 26197 if (((_this$hls$userConfig = this.hls.userConfig) == null ? void 0 : _this$hls$userConfig.abrEwmaDefaultEstimate) === undefined) { 26198 const startingBwEstimate = Math.min(firstLevelBitrate, this.hls.config.abrEwmaDefaultEstimateMax); 26199 if (startingBwEstimate > bandwidthEstimate && bandwidthEstimate === hlsDefaultConfig.abrEwmaDefaultEstimate) { 26200 this.hls.bandwidthEstimate = startingBwEstimate; 26201 } 26202 } 26203 break; 26204 } 26205 } 26206 26207 // Audio is only alternate if manifest include a URI along with the audio group tag, 26208 // and this is not an audio-only stream where levels contain audio-only 26209 const audioOnly = audioCodecFound && !videoCodecFound; 26210 const edata = { 26211 levels, 26212 audioTracks, 26213 subtitleTracks, 26214 sessionData: data.sessionData, 26215 sessionKeys: data.sessionKeys, 26216 firstLevel: this._firstLevel, 26217 stats: data.stats, 26218 audio: audioCodecFound, 26219 video: videoCodecFound, 26220 altAudio: !audioOnly && audioTracks.some(t => !!t.url) 26221 }; 26222 this.hls.trigger(Events.MANIFEST_PARSED, edata); 26223 26224 // Initiate loading after all controllers have received MANIFEST_PARSED 26225 if (this.hls.config.autoStartLoad || this.hls.forceStartLoad) { 26226 this.hls.startLoad(this.hls.config.startPosition); 26227 } 26228 } 26229 get levels() { 26230 if (this._levels.length === 0) { 26231 return null; 26232 } 26233 return this._levels; 26234 } 26235 get level() { 26236 return this.currentLevelIndex; 26237 } 26238 set level(newLevel) { 26239 const levels = this._levels; 26240 if (levels.length === 0) { 26241 return; 26242 } 26243 // check if level idx is valid 26244 if (newLevel < 0 || newLevel >= levels.length) { 26245 // invalid level id given, trigger error 26246 const error = new Error('invalid level idx'); 26247 const fatal = newLevel < 0; 26248 this.hls.trigger(Events.ERROR, { 26249 type: ErrorTypes.OTHER_ERROR, 26250 details: ErrorDetails.LEVEL_SWITCH_ERROR, 26251 level: newLevel, 26252 fatal, 26253 error, 26254 reason: error.message 26255 }); 26256 if (fatal) { 26257 return; 26258 } 26259 newLevel = Math.min(newLevel, levels.length - 1); 26260 } 26261 const lastLevelIndex = this.currentLevelIndex; 26262 const lastLevel = this.currentLevel; 26263 const lastPathwayId = lastLevel ? lastLevel.attrs['PATHWAY-ID'] : undefined; 26264 const level = levels[newLevel]; 26265 const pathwayId = level.attrs['PATHWAY-ID']; 26266 this.currentLevelIndex = newLevel; 26267 this.currentLevel = level; 26268 if (lastLevelIndex === newLevel && level.details && lastLevel && lastPathwayId === pathwayId) { 26269 return; 26270 } 26271 this.log(`Switching to level ${newLevel} (${level.height ? level.height + 'p ' : ''}${level.videoRange ? level.videoRange + ' ' : ''}${level.codecSet ? level.codecSet + ' ' : ''}@${level.bitrate})${pathwayId ? ' with Pathway ' + pathwayId : ''} from level ${lastLevelIndex}${lastPathwayId ? ' with Pathway ' + lastPathwayId : ''}`); 26272 const levelSwitchingData = { 26273 level: newLevel, 26274 attrs: level.attrs, 26275 details: level.details, 26276 bitrate: level.bitrate, 26277 averageBitrate: level.averageBitrate, 26278 maxBitrate: level.maxBitrate, 26279 realBitrate: level.realBitrate, 26280 width: level.width, 26281 height: level.height, 26282 codecSet: level.codecSet, 26283 audioCodec: level.audioCodec, 26284 videoCodec: level.videoCodec, 26285 audioGroups: level.audioGroups, 26286 subtitleGroups: level.subtitleGroups, 26287 loaded: level.loaded, 26288 loadError: level.loadError, 26289 fragmentError: level.fragmentError, 26290 name: level.name, 26291 id: level.id, 26292 uri: level.uri, 26293 url: level.url, 26294 urlId: 0, 26295 audioGroupIds: level.audioGroupIds, 26296 textGroupIds: level.textGroupIds 26297 }; 26298 this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData); 26299 // check if we need to load playlist for this level 26300 const levelDetails = level.details; 26301 if (!levelDetails || levelDetails.live) { 26302 // level not retrieved yet, or live playlist we need to (re)load it 26303 const hlsUrlParameters = this.switchParams(level.uri, lastLevel == null ? void 0 : lastLevel.details, levelDetails); 26304 this.loadPlaylist(hlsUrlParameters); 26305 } 26306 } 26307 get manualLevel() { 26308 return this.manualLevelIndex; 26309 } 26310 set manualLevel(newLevel) { 26311 this.manualLevelIndex = newLevel; 26312 if (this._startLevel === undefined) { 26313 this._startLevel = newLevel; 26314 } 26315 if (newLevel !== -1) { 26316 this.level = newLevel; 26317 } 26318 } 26319 get firstLevel() { 26320 return this._firstLevel; 26321 } 26322 set firstLevel(newLevel) { 26323 this._firstLevel = newLevel; 26324 } 26325 get startLevel() { 26326 // Setting hls.startLevel (this._startLevel) overrides config.startLevel 26327 if (this._startLevel === undefined) { 26328 const configStartLevel = this.hls.config.startLevel; 26329 if (configStartLevel !== undefined) { 26330 return configStartLevel; 26331 } 26332 return this.hls.firstAutoLevel; 26333 } 26334 return this._startLevel; 26335 } 26336 set startLevel(newLevel) { 26337 this._startLevel = newLevel; 26338 } 26339 onError(event, data) { 26340 if (data.fatal || !data.context) { 26341 return; 26342 } 26343 if (data.context.type === PlaylistContextType.LEVEL && data.context.level === this.level) { 26344 this.checkRetry(data); 26345 } 26346 } 26347 26348 // reset errors on the successful load of a fragment 26349 onFragBuffered(event, { 26350 frag 26351 }) { 26352 if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) { 26353 const el = frag.elementaryStreams; 26354 if (!Object.keys(el).some(type => !!el[type])) { 26355 return; 26356 } 26357 const level = this._levels[frag.level]; 26358 if (level != null && level.loadError) { 26359 this.log(`Resetting level error count of ${level.loadError} on frag buffered`); 26360 level.loadError = 0; 26361 } 26362 } 26363 } 26364 onLevelLoaded(event, data) { 26365 var _data$deliveryDirecti2; 26366 const { 26367 level, 26368 details 26369 } = data; 26370 const curLevel = this._levels[level]; 26371 if (!curLevel) { 26372 var _data$deliveryDirecti; 26373 this.warn(`Invalid level index ${level}`); 26374 if ((_data$deliveryDirecti = data.deliveryDirectives) != null && _data$deliveryDirecti.skip) { 26375 details.deltaUpdateFailed = true; 26376 } 26377 return; 26378 } 26379 26380 // only process level loaded events matching with expected level 26381 if (level === this.currentLevelIndex) { 26382 // reset level load error counter on successful level loaded only if there is no issues with fragments 26383 if (curLevel.fragmentError === 0) { 26384 curLevel.loadError = 0; 26385 } 26386 this.playlistLoaded(level, data, curLevel.details); 26387 } else if ((_data$deliveryDirecti2 = data.deliveryDirectives) != null && _data$deliveryDirecti2.skip) { 26388 // received a delta playlist update that cannot be merged 26389 details.deltaUpdateFailed = true; 26390 } 26391 } 26392 loadPlaylist(hlsUrlParameters) { 26393 super.loadPlaylist(); 26394 const currentLevelIndex = this.currentLevelIndex; 26395 const currentLevel = this.currentLevel; 26396 if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { 26397 let url = currentLevel.uri; 26398 if (hlsUrlParameters) { 26399 try { 26400 url = hlsUrlParameters.addDirectives(url); 26401 } catch (error) { 26402 this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); 26403 } 26404 } 26405 const pathwayId = currentLevel.attrs['PATHWAY-ID']; 26406 this.log(`Loading level index ${currentLevelIndex}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''} with${pathwayId ? ' Pathway ' + pathwayId : ''} ${url}`); 26407 26408 // console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId); 26409 // console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level); 26410 this.clearTimer(); 26411 this.hls.trigger(Events.LEVEL_LOADING, { 26412 url, 26413 level: currentLevelIndex, 26414 pathwayId: currentLevel.attrs['PATHWAY-ID'], 26415 id: 0, 26416 // Deprecated Level urlId 26417 deliveryDirectives: hlsUrlParameters || null 26418 }); 26419 } 26420 } 26421 get nextLoadLevel() { 26422 if (this.manualLevelIndex !== -1) { 26423 return this.manualLevelIndex; 26424 } else { 26425 return this.hls.nextAutoLevel; 26426 } 26427 } 26428 set nextLoadLevel(nextLevel) { 26429 this.level = nextLevel; 26430 if (this.manualLevelIndex === -1) { 26431 this.hls.nextAutoLevel = nextLevel; 26432 } 26433 } 26434 removeLevel(levelIndex) { 26435 var _this$currentLevel; 26436 const levels = this._levels.filter((level, index) => { 26437 if (index !== levelIndex) { 26438 return true; 26439 } 26440 if (this.steering) { 26441 this.steering.removeLevel(level); 26442 } 26443 if (level === this.currentLevel) { 26444 this.currentLevel = null; 26445 this.currentLevelIndex = -1; 26446 if (level.details) {
26447 level.details.fragments.forEach(f => f.level = -1); 26448 } 26449 } 26450 return false; 26451 }); 26452 reassignFragmentLevelIndexes(levels); 26453 this._levels = levels; 26454 if (this.currentLevelIndex > -1 && (_this$currentLevel = this.currentLevel) != null && _this$currentLevel.details) { 26455 this.currentLevelIndex = this.currentLevel.details.fragments[0].level; 26456 } 26457 this.hls.trigger(Events.LEVELS_UPDATED, { 26458 levels 26459 }); 26460 } 26461 onLevelsUpdated(event, { 26462 levels 26463 }) { 26464 this._levels = levels; 26465 } 26466 checkMaxAutoUpdated() { 26467 const { 26468 autoLevelCapping, 26469 maxAutoLevel, 26470 maxHdcpLevel 26471 } = this.hls; 26472 if (this._maxAutoLevel !== maxAutoLevel) { 26473 this._maxAutoLevel = maxAutoLevel; 26474 this.hls.trigger(Events.MAX_AUTO_LEVEL_UPDATED, { 26475 autoLevelCapping, 26476 levels: this.levels, 26477 maxAutoLevel, 26478 minAutoLevel: this.hls.minAutoLevel, 26479 maxHdcpLevel 26480 }); 26481 } 26482 } 26483} 26484function assignTrackIdsByGroup(tracks) { 26485 const groups = {}; 26486 tracks.forEach(track => { 26487 const groupId = track.groupId || ''; 26488 track.id = groups[groupId] = groups[groupId] || 0; 26489 groups[groupId]++; 26490 }); 26491} 26492 26493class KeyLoader { 26494 constructor(config) { 26495 this.config = void 0; 26496 this.keyUriToKeyInfo = {}; 26497 this.emeController = null; 26498 this.config = config; 26499 } 26500 abort(type) { 26501 for (const uri in this.keyUriToKeyInfo) { 26502 const loader = this.keyUriToKeyInfo[uri].loader; 26503 if (loader) { 26504 var _loader$context; 26505 if (type && type !== ((_loader$context = loader.context) == null ? void 0 : _loader$context.frag.type)) { 26506 return; 26507 } 26508 loader.abort(); 26509 } 26510 } 26511 } 26512 detach() { 26513 for (const uri in this.keyUriToKeyInfo) { 26514 const keyInfo = this.keyUriToKeyInfo[uri]; 26515 // Remove cached EME keys on detach 26516 if (keyInfo.mediaKeySessionContext || keyInfo.decryptdata.isCommonEncryption) { 26517 delete this.keyUriToKeyInfo[uri]; 26518 } 26519 } 26520 } 26521 destroy() { 26522 this.detach(); 26523 for (const uri in this.keyUriToKeyInfo) { 26524 const loader = this.keyUriToKeyInfo[uri].loader; 26525 if (loader) { 26526 loader.destroy(); 26527 } 26528 } 26529 this.keyUriToKeyInfo = {}; 26530 } 26531 createKeyLoadError(frag, details = ErrorDetails.KEY_LOAD_ERROR, error, networkDetails, response) { 26532 return new LoadError({ 26533 type: ErrorTypes.NETWORK_ERROR, 26534 details, 26535 fatal: false, 26536 frag, 26537 response, 26538 error, 26539 networkDetails 26540 }); 26541 } 26542 loadClear(loadingFrag, encryptedFragments) { 26543 if (this.emeController && this.config.emeEnabled) { 26544 // access key-system with nearest key on start (loaidng frag is unencrypted) 26545 const { 26546 sn, 26547 cc 26548 } = loadingFrag; 26549 for (let i = 0; i < encryptedFragments.length; i++) { 26550 const frag = encryptedFragments[i]; 26551 if (cc <= frag.cc && (sn === 'initSegment' || frag.sn === 'initSegment' || sn < frag.sn)) { 26552 this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { 26553 frag.setKeyFormat(keySystemFormat); 26554 }); 26555 break; 26556 } 26557 } 26558 } 26559 } 26560 load(frag) { 26561 if (!frag.decryptdata && frag.encrypted && this.emeController) { 26562 // Multiple keys, but none selected, resolve in eme-controller 26563 return this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { 26564 return this.loadInternal(frag, keySystemFormat); 26565 }); 26566 } 26567 return this.loadInternal(frag); 26568 } 26569 loadInternal(frag, keySystemFormat) { 26570 var _keyInfo, _keyInfo2; 26571 if (keySystemFormat) { 26572 frag.setKeyFormat(keySystemFormat); 26573 } 26574 const decryptdata = frag.decryptdata; 26575 if (!decryptdata) { 26576 const error = new Error(keySystemFormat ? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}` : 'Missing decryption data on fragment in onKeyLoading'); 26577 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, error)); 26578 } 26579 const uri = decryptdata.uri; 26580 if (!uri) { 26581 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Invalid key URI: "${uri}"`))); 26582 } 26583 let keyInfo = this.keyUriToKeyInfo[uri]; 26584 if ((_keyInfo = keyInfo) != null && _keyInfo.decryptdata.key) { 26585 decryptdata.key = keyInfo.decryptdata.key; 26586 return Promise.resolve({ 26587 frag, 26588 keyInfo 26589 }); 26590 } 26591 // Return key load promise as long as it does not have a mediakey session with an unusable key status
26592 if ((_keyInfo2 = keyInfo) != null && _keyInfo2.keyLoadPromise) { 26593 var _keyInfo$mediaKeySess; 26594 switch ((_keyInfo$mediaKeySess = keyInfo.mediaKeySessionContext) == null ? void 0 : _keyInfo$mediaKeySess.keyStatus) { 26595 case undefined: 26596 case 'status-pending': 26597 case 'usable': 26598 case 'usable-in-future': 26599 return keyInfo.keyLoadPromise.then(keyLoadedData => { 26600 // Return the correct fragment with updated decryptdata key and loaded keyInfo 26601 decryptdata.key = keyLoadedData.keyInfo.decryptdata.key; 26602 return { 26603 frag, 26604 keyInfo 26605 }; 26606 }); 26607 } 26608 // If we have a key session and status and it is not pending or usable, continue 26609 // This will go back to the eme-controller for expired keys to get a new keyLoadPromise 26610 } 26611 26612 // Load the key or return the loading promise 26613 keyInfo = this.keyUriToKeyInfo[uri] = { 26614 decryptdata, 26615 keyLoadPromise: null, 26616 loader: null, 26617 mediaKeySessionContext: null 26618 }; 26619 switch (decryptdata.method) { 26620 case 'ISO-23001-7': 26621 case 'SAMPLE-AES': 26622 case 'SAMPLE-AES-CENC': 26623 case 'SAMPLE-AES-CTR': 26624 if (decryptdata.keyFormat === 'identity') { 26625 // loadKeyHTTP handles http(s) and data URLs 26626 return this.loadKeyHTTP(keyInfo, frag); 26627 } 26628 return this.loadKeyEME(keyInfo, frag); 26629 case 'AES-128': 26630 return this.loadKeyHTTP(keyInfo, frag); 26631 default: 26632 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Key supplied with unsupported METHOD: "${decryptdata.method}"`))); 26633 } 26634 } 26635 loadKeyEME(keyInfo, frag) { 26636 const keyLoadedData = { 26637 frag, 26638 keyInfo 26639 }; 26640 if (this.emeController && this.config.emeEnabled) { 26641 const keySessionContextPromise = this.emeController.loadKey(keyLoadedData); 26642 if (keySessionContextPromise) { 26643 return (keyInfo.keyLoadPromise = keySessionContextPromise.then(keySessionContext => { 26644 keyInfo.mediaKeySessionContext = keySessionContext; 26645 return keyLoadedData; 26646 })).catch(error => { 26647 // Remove promise for license renewal or retry 26648 keyInfo.keyLoadPromise = null; 26649 throw error; 26650 }); 26651 } 26652 } 26653 return Promise.resolve(keyLoadedData); 26654 } 26655 loadKeyHTTP(keyInfo, frag) { 26656 const config = this.config; 26657 const Loader = config.loader; 26658 const keyLoader = new Loader(config); 26659 frag.keyLoader = keyInfo.loader = keyLoader; 26660 return keyInfo.keyLoadPromise = new Promise((resolve, reject) => { 26661 const loaderContext = { 26662 keyInfo, 26663 frag, 26664 responseType: 'arraybuffer', 26665 url: keyInfo.decryptdata.uri 26666 }; 26667 26668 // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times, 26669 // key-loader will trigger an error and rely on stream-controller to handle retry logic. 26670 // this will also align retry logic with fragment-loader 26671 const loadPolicy = config.keyLoadPolicy.default; 26672 const loaderConfig = { 26673 loadPolicy, 26674 timeout: loadPolicy.maxLoadTimeMs, 26675 maxRetry: 0, 26676 retryDelay: 0, 26677 maxRetryDelay: 0 26678 }; 26679 const loaderCallbacks = { 26680 onSuccess: (response, stats, context, networkDetails) => { 26681 const { 26682 frag, 26683 keyInfo, 26684 url: uri 26685 } = context; 26686 if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) { 26687 return reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error('after key load, decryptdata unset or changed'), networkDetails)); 26688 } 26689 keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(response.data); 26690 26691 // detach fragment key loader on load success 26692 frag.keyLoader = null; 26693 keyInfo.loader = null; 26694 resolve({ 26695 frag, 26696 keyInfo 26697 }); 26698 }, 26699 onError: (response, context, networkDetails, stats) => { 26700 this.resetLoader(context); 26701 reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`HTTP Error ${response.code} loading key ${response.text}
26701`), networkDetails, _objectSpread2({ 26702 url: loaderContext.url, 26703 data: undefined 26704 }, response))); 26705 }, 26706 onTimeout: (stats, context, networkDetails) => { 26707 this.resetLoader(context); 26708 reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_TIMEOUT, new Error('key loading timed out'), networkDetails)); 26709 }, 26710 onAbort: (stats, context, networkDetails) => { 26711 this.resetLoader(context); 26712 reject(this.createKeyLoadError(frag, ErrorDetails.INTERNAL_ABORTED, new Error('key loading aborted'), networkDetails)); 26713 } 26714 }; 26715 keyLoader.load(loaderContext, loaderConfig, loaderCallbacks); 26716 }); 26717 } 26718 resetLoader(context) { 26719 const { 26720 frag, 26721 keyInfo, 26722 url: uri 26723 } = context; 26724 const loader = keyInfo.loader; 26725 if (frag.keyLoader === loader) { 26726 frag.keyLoader = null; 26727 keyInfo.loader = null; 26728 } 26729 delete this.keyUriToKeyInfo[uri]; 26730 if (loader) { 26731 loader.destroy(); 26732 } 26733 } 26734} 26735 26736function getSourceBuffer() { 26737 return self.SourceBuffer || self.WebKitSourceBuffer; 26738} 26739function isMSESupported() { 26740 const mediaSource = getMediaSource(); 26741 if (!mediaSource) { 26742 return false; 26743 } 26744 26745 // if SourceBuffer is exposed ensure its API is valid 26746 // Older browsers do not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible 26747 const sourceBuffer = getSourceBuffer(); 26748 return !sourceBuffer || sourceBuffer.prototype && typeof sourceBuffer.prototype.appendBuffer === 'function' && typeof sourceBuffer.prototype.remove === 'function'; 26749} 26750function isSupported() { 26751 if (!isMSESupported()) { 26752 return false; 26753 } 26754 const mediaSource = getMediaSource(); 26755 return typeof (mediaSource == null ? void 0 : mediaSource.isTypeSupported) === 'function' && (['avc1.42E01E,mp4a.40.2', 'av01.0.01M.08', 'vp09.00.50.08'].some(codecsForVideoContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecsForVideoContainer, 'video'))) || ['mp4a.40.2', 'fLaC'].some(codecForAudioContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecForAudioContainer, 'audio')))); 26756} 26757function changeTypeSupported() { 26758 var _sourceBuffer$prototy; 26759 const sourceBuffer = getSourceBuffer(); 26760 return typeof (sourceBuffer == null ? void 0 : (_sourceBuffer$prototy = sourceBuffer.prototype) == null ? void 0 : _sourceBuffer$prototy.changeType) === 'function'; 26761} 26762 26763const STALL_MINIMUM_DURATION_MS = 250; 26764const MAX_START_GAP_JUMP = 2.0; 26765const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1; 26766const SKIP_BUFFER_RANGE_START = 0.05; 26767class GapController { 26768 constructor(config, media, fragmentTracker, hls) { 26769 this.config = void 0; 26770 this.media = null; 26771 this.fragmentTracker = void 0; 26772 this.hls = void 0; 26773 this.nudgeRetry = 0; 26774 this.stallReported = false; 26775 this.stalled = null; 26776 this.moved = false; 26777 this.seeking = false; 26778 this.config = config; 26779 this.media = media; 26780 this.fragmentTracker = fragmentTracker; 26781 this.hls = hls; 26782 } 26783 destroy() { 26784 this.media = null; 26785 // @ts-ignore 26786 this.hls = this.fragmentTracker = null; 26787 } 26788 26789 /** 26790 * Checks if the playhead is stuck within a gap, and if so, attempts to free it. 26791 * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range). 26792 * 26793 * @param lastCurrentTime - Previously read playhead position 26794 */ 26795 poll(lastCurrentTime, activeFrag) { 26796 const { 26797 config, 26798 media, 26799 stalled 26800 } = this; 26801 if (media === null) { 26802 return; 26803 } 26804 const { 26805 currentTime, 26806 seeking 26807 } = media; 26808 const seeked = this.seeking && !seeking; 26809 const beginSeek = !this.seeking && seeking; 26810 this.seeking = seeking; 26811 26812 // The playhead is moving, no-op 26813 if (currentTime !== lastCurrentTime) { 26814 this.moved = true; 26815 if (!seeking) { 26816 this.nudgeRetry = 0; 26817 } 26818 if (stalled !== null) { 26819 // The playhead is now moving, but was previously stalled 26820 if (this.stallReported) { 26821 const _stalledDuration = self.performance.now() - stalled; 26822 logger.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(_stalledDuration)}ms`); 26823 this.stallReported = false;
26824 } 26825 this.stalled = null; 26826 } 26827 return; 26828 } 26829 26830 // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek 26831 if (beginSeek || seeked) { 26832 this.stalled = null; 26833 return; 26834 } 26835 26836 // The playhead should not be moving 26837 if (media.paused && !seeking || media.ended || media.playbackRate === 0 || !BufferHelper.getBuffered(media).length) { 26838 this.nudgeRetry = 0; 26839 return; 26840 } 26841 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 26842 const nextStart = bufferInfo.nextStart || 0; 26843 if (seeking) { 26844 // Waiting for seeking in a buffered range to complete 26845 const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP; 26846 // Next buffered range is too far ahead to jump to while still seeking 26847 const noBufferGap = !nextStart || activeFrag && activeFrag.start <= currentTime || nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime); 26848 if (hasEnoughBuffer || noBufferGap) { 26849 return; 26850 } 26851 // Reset moved state when seeking to a point in or before a gap 26852 this.moved = false; 26853 } 26854 26855 // Skip start gaps if we haven't played, but the last poll detected the start of a stall 26856 // The addition poll gives the browser a chance to jump the gap for us 26857 if (!this.moved && this.stalled !== null) { 26858 var _level$details; 26859 // There is no playable buffer (seeked, waiting for buffer) 26860 const isBuffered = bufferInfo.len > 0; 26861 if (!isBuffered && !nextStart) { 26862 return; 26863 } 26864 // Jump start gaps within jump threshold 26865 const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime; 26866 26867 // When joining a live stream with audio tracks, account for live playlist window sliding by allowing 26868 // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment 26869 // that begins over 1 target duration after the video start position. 26870 const level = this.hls.levels ? this.hls.levels[this.hls.currentLevel] : null; 26871 const isLive = level == null ? void 0 : (_level$details = level.details) == null ? void 0 : _level$details.live; 26872 const maxStartGapJump = isLive ? level.details.targetduration * 2 : MAX_START_GAP_JUMP; 26873 const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime); 26874 if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) { 26875 if (!media.paused) { 26876 this._trySkipBufferHole(partialOrGap); 26877 } 26878 return; 26879 } 26880 } 26881 26882 // Start tracking stall time 26883 const tnow = self.performance.now(); 26884 if (stalled === null) { 26885 this.stalled = tnow; 26886 return; 26887 } 26888 const stalledDuration = tnow - stalled; 26889 if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) { 26890 // Report stalling after trying to fix 26891 this._reportStall(bufferInfo); 26892 if (!this.media) { 26893 return; 26894 } 26895 } 26896 const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole); 26897 this._tryFixBufferStall(bufferedWithHoles, stalledDuration); 26898 } 26899 26900 /** 26901 * Detects and attempts to fix known buffer stalling issues. 26902 * @param bufferInfo - The properties of the current buffer. 26903 * @param stalledDurationMs - The amount of time Hls.js has been stalling for. 26904 * @private 26905 */ 26906 _tryFixBufferStall(bufferInfo, stalledDurationMs) { 26907 const { 26908 config, 26909 fragmentTracker, 26910 media 26911 } = this; 26912 if (media === null) { 26913 return; 26914 } 26915 const currentTime = media.currentTime; 26916 const partial = fragmentTracker.getPartialFragment(currentTime); 26917 if (partial) { 26918 // Try to skip over the buffer hole caused by a partial fragment 26919 // This method isn't limited by the size of the gap between buffered ranges 26920 const targetTime = this._trySkipBufferHole(partial); 26921 // we return here in this case, meaning 26922 // the branch below only executes when we haven't seeked to a new position 26923 if (targetTime || !this.media) { 26924 return; 26925 } 26926 } 26927 26928 // if we haven't had to skip over a buffer hole of a partial fragment 26929 // we may just have to "nudge" the playlist as the browser decoding/rendering engine
26930 // needs to cross some sort of threshold covering all source-buffers content 26931 // to start playing properly. 26932 if ((bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && bufferInfo.nextStart - currentTime < config.maxBufferHole) && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) { 26933 logger.warn('Trying to nudge playhead over buffer-hole'); 26934 // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds 26935 // We only try to jump the hole if it's under the configured size 26936 // Reset stalled so to rearm watchdog timer 26937 this.stalled = null; 26938 this._tryNudgeBuffer(); 26939 } 26940 } 26941 26942 /** 26943 * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period. 26944 * @param bufferLen - The playhead distance from the end of the current buffer segment. 26945 * @private 26946 */ 26947 _reportStall(bufferInfo) { 26948 const { 26949 hls, 26950 media, 26951 stallReported 26952 } = this; 26953 if (!stallReported && media) { 26954 // Report stalled error once 26955 this.stallReported = true; 26956 const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${JSON.stringify(bufferInfo)})`); 26957 logger.warn(error.message); 26958 hls.trigger(Events.ERROR, { 26959 type: ErrorTypes.MEDIA_ERROR, 26960 details: ErrorDetails.BUFFER_STALLED_ERROR, 26961 fatal: false, 26962 error, 26963 buffer: bufferInfo.len 26964 }); 26965 } 26966 } 26967 26968 /** 26969 * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments 26970 * @param partial - The partial fragment found at the current time (where playback is stalling). 26971 * @private 26972 */ 26973 _trySkipBufferHole(partial) { 26974 const { 26975 config, 26976 hls, 26977 media 26978 } = this; 26979 if (media === null) { 26980 return 0; 26981 } 26982 26983 // Check if currentTime is between unbuffered regions of partial fragments 26984 const currentTime = media.currentTime; 26985 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 26986 const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart; 26987 if (startTime) { 26988 const bufferStarved = bufferInfo.len <= config.maxBufferHole; 26989 const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3; 26990 const gapLength = startTime - currentTime; 26991 if (gapLength > 0 && (bufferStarved || waiting)) { 26992 // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial 26993 if (gapLength > config.maxBufferHole) { 26994 const { 26995 fragmentTracker 26996 } = this; 26997 let startGap = false; 26998 if (currentTime === 0) { 26999 const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN); 27000 if (startFrag && startTime < startFrag.end) { 27001 startGap = true; 27002 } 27003 } 27004 if (!startGap) { 27005 const startProvisioned = partial || fragmentTracker.getAppendedFrag(currentTime, PlaylistLevelType.MAIN); 27006 if (startProvisioned) { 27007 let moreToLoad = false; 27008 let pos = startProvisioned.end; 27009 while (pos < startTime) { 27010 const provisioned = fragmentTracker.getPartialFragment(pos); 27011 if (provisioned) { 27012 pos += provisioned.duration; 27013 } else { 27014 moreToLoad = true; 27015 break; 27016 } 27017 } 27018 if (moreToLoad) { 27019 return 0; 27020 } 27021 } 27022 } 27023 } 27024 const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS); 27025 logger.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`); 27026 this.moved = true; 27027 this.stalled = null; 27028 media.currentTime = targetTime; 27029 if (partial && !partial.gap) { 27030 const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`); 27031 hls.trigger(Events.ERROR, { 27032 type: ErrorTypes.MEDIA_ERROR, 27033 details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, 27034 fatal: false, 27035 error, 27036 reason: error.message, 27037 frag: partial 27038 }); 27039 } 27040 return targetTime; 27041 } 27042 } 27043 return 0; 27044 } 27045 27046 /** 27047 * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount. 27048 * @private 27049 */ 27050 _tryNudgeBuffer() { 27051 const { 27052 config, 27053 hls, 27054 media, 27055 nudgeRetry 27056 } = this; 27057 if (media === null) { 27058 return; 27059 } 27060 const currentTime = media.currentTime; 27061 this.nudgeRetry++; 27062 if (nudgeRetry < config.nudgeMaxRetry) { 27063 const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset; 27064 // playback stalled in buffered area ... let's nudge currentTime to try to overcome this 27065 const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`); 27066 logger.warn(error.message); 27067 media.currentTime = targetTime; 27068 hls.trigger(Events.ERROR, { 27069 type: ErrorTypes.MEDIA_ERROR, 27070 details: ErrorDetails.BUFFER_NUDGE_ON_STALL, 27071 error, 27072 fatal: false 27073 }); 27074 } else { 27075 const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`); 27076 logger.error(error.message); 27077 hls.trigger(Events.ERROR, { 27078 type: ErrorTypes.MEDIA_ERROR, 27079 details: ErrorDetails.BUFFER_STALLED_ERROR, 27080 error, 27081 fatal: true 27082 }); 27083 } 27084 } 27085} 27086 27087const TICK_INTERVAL = 100; // how often to tick in ms 27088 27089class StreamController extends BaseStreamController { 27090 constructor(hls, fragmentTracker, keyLoader) { 27091 super(hls, fragmentTracker, keyLoader, '[stream-controller]', PlaylistLevelType.MAIN); 27092 this.audioCodecSwap = false;
vendor: 4,333 bytes, lines 27093-27202
27093 this.gapController = null; 27094 this.level = -1; 27095 this._forceStartLoad = false; 27096 this.altAudio = false; 27097 this.audioOnly = false; 27098 this.fragPlaying = null; 27099 this.onvplaying = null; 27100 this.onvseeked = null; 27101 this.fragLastKbps = 0; 27102 this.couldBacktrack = false; 27103 this.backtrackFragment = null; 27104 this.audioCodecSwitch = false; 27105 this.videoBuffer = null; 27106 this._registerListeners(); 27107 } 27108 _registerListeners() { 27109 const { 27110 hls 27111 } = this; 27112 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 27113 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 27114 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 27115 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 27116 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 27117 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 27118 hls.on(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); 27119 hls.on(Events.ERROR, this.onError, this); 27120 hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 27121 hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); 27122 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 27123 hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 27124 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 27125 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 27126 } 27127 _unregisterListeners() { 27128 const { 27129 hls 27130 } = this; 27131 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 27132 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 27133 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 27134 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 27135 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 27136 hls.off(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); 27137 hls.off(Events.ERROR, this.onError, this); 27138 hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 27139 hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); 27140 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 27141 hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 27142 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 27143 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 27144 } 27145 onHandlerDestroying() { 27146 this._unregisterListeners(); 27147 super.onHandlerDestroying(); 27148 } 27149 startLoad(startPosition) { 27150 if (this.levels) { 27151 const { 27152 lastCurrentTime, 27153 hls 27154 } = this; 27155 this.stopLoad(); 27156 this.setInterval(TICK_INTERVAL); 27157 this.level = -1; 27158 if (!this.startFragRequested) { 27159 // determine load level 27160 let startLevel = hls.startLevel; 27161 if (startLevel === -1) { 27162 if (hls.config.testBandwidth && this.levels.length > 1) { 27163 // -1 : guess start Level by doing a bitrate test by loading first fragment of lowest quality level 27164 startLevel = 0; 27165 this.bitrateTest = true; 27166 } else { 27167 startLevel = hls.firstAutoLevel; 27168 } 27169 } 27170 // set new level to playlist loader : this will trigger start level load 27171 // hls.nextLoadLevel remains until it is set to a new value or until a new frag is successfully loaded 27172 hls.nextLoadLevel = startLevel; 27173 this.level = hls.loadLevel; 27174 this.loadedmetadata = false; 27175 } 27176 // if startPosition undefined but lastCurrentTime set, set startPosition to last currentTime 27177 if (lastCurrentTime > 0 && startPosition === -1) { 27178 this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); 27179 startPosition = lastCurrentTime; 27180 } 27181 this.state = State.IDLE; 27182 this.nextLoadPosition = this.startPosition = this.lastCurrentTime = startPosition; 27183 this.tick(); 27184 } else { 27185 this._forceStartLoad = true; 27186 this.state = State.STOPPED; 27187 } 27188 } 27189 stopLoad() { 27190 this._forceStartLoad = false; 27191 super.stopLoad(); 27192 } 27193 doTick() { 27194 switch (this.state) { 27195 case State.WAITING_LEVEL: 27196 { 27197 const { 27198 levels, 27199 level 27200 } = this; 27201 const currentLevel = levels == null ? void 0 : levels[level]; 27202 const details = currentLevel == null ? void 0 : currentLevel.details;
27203 if (details && (!details.live || this.levelLastLoaded === currentLevel)) { 27204 if (this.waitForCdnTuneIn(details)) { 27205 break; 27206 } 27207 this.state = State.IDLE; 27208 break; 27209 } else if (this.hls.nextLoadLevel !== this.level) { 27210 this.state = State.IDLE; 27211 break; 27212 } 27213 break; 27214 } 27215 case State.FRAG_LOADING_WAITING_RETRY: 27216 { 27217 var _this$media; 27218 const now = self.performance.now(); 27219 const retryDate = this.retryDate; 27220 // if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading 27221 if (!retryDate || now >= retryDate || (_this$media = this.media) != null && _this$media.seeking) { 27222 const { 27223 levels, 27224 level 27225 } = this; 27226 const currentLevel = levels == null ? void 0 : levels[level]; 27227 this.resetStartWhenNotLoaded(currentLevel || null); 27228 this.state = State.IDLE; 27229 } 27230 } 27231 break; 27232 } 27233 if (this.state === State.IDLE) { 27234 this.doTickIdle(); 27235 } 27236 this.onTickEnd(); 27237 } 27238 onTickEnd() { 27239 super.onTickEnd(); 27240 this.checkBuffer(); 27241 this.checkFragmentChanged(); 27242 } 27243 doTickIdle() { 27244 const { 27245 hls, 27246 levelLastLoaded, 27247 levels, 27248 media 27249 } = this; 27250 27251 // if start level not parsed yet OR 27252 // if video not attached AND start fragment already requested OR start frag prefetch not enabled 27253 // exit loop, as we either need more info (level not parsed) or we need media to be attached to load new fragment 27254 if (levelLastLoaded === null || !media && (this.startFragRequested || !hls.config.startFragPrefetch)) { 27255 return; 27256 } 27257 27258 // If the "main" level is audio-only but we are loading an alternate track in the same group, do not load anything 27259 if (this.altAudio && this.audioOnly) { 27260 return; 27261 } 27262 const level = hls.nextLoadLevel; 27263 if (!(levels != null && levels[level])) { 27264 return; 27265 } 27266 const levelInfo = levels[level]; 27267 27268 // if buffer length is less than maxBufLen try to load a new fragment 27269 27270 const bufferInfo = this.getMainFwdBufferInfo(); 27271 if (bufferInfo === null) { 27272 return; 27273 } 27274 const lastDetails = this.getLevelDetails(); 27275 if (lastDetails && this._streamEnded(bufferInfo, lastDetails)) { 27276 const data = {}; 27277 if (this.altAudio) { 27278 data.type = 'video'; 27279 } 27280 this.hls.trigger(Events.BUFFER_EOS, data); 27281 this.state = State.ENDED; 27282 return; 27283 } 27284 27285 // set next load level : this will trigger a playlist load if needed 27286 if (hls.loadLevel !== level && hls.manualLevel === -1) { 27287 this.log(`Adapting to level ${level} from level ${this.level}`); 27288 } 27289 this.level = hls.nextLoadLevel = level; 27290 const levelDetails = levelInfo.details; 27291 // if level info not retrieved yet, switch state and wait for level retrieval 27292 // if live playlist, ensure that new playlist has been refreshed to avoid loading/try to load 27293 // a useless and outdated fragment (that might even introduce load error if it is already out of the live playlist) 27294 if (!levelDetails || this.state === State.WAITING_LEVEL || levelDetails.live && this.levelLastLoaded !== levelInfo) { 27295 this.level = level; 27296 this.state = State.WAITING_LEVEL; 27297 return; 27298 } 27299 const bufferLen = bufferInfo.len; 27300 27301 // compute max Buffer Length that we could get from this load level, based on level bitrate. don't buffer more than 60 MB and more than 30s 27302 const maxBufLen = this.getMaxBufferLength(levelInfo.maxBitrate); 27303 27304 // Stay idle if we are still with buffer margins 27305 if (bufferLen >= maxBufLen) { 27306 return; 27307 } 27308 if (this.backtrackFragment && this.backtrackFragment.start > bufferInfo.end) { 27309 this.backtrackFragment = null; 27310 } 27311 const targetBufferTime = this.backtrackFragment ? this.backtrackFragment.start : bufferInfo.end; 27312 let frag = this.getNextFragment(targetBufferTime, levelDetails); 27313 // Avoid backtracking by loading an earlier segment in streams with segments that do not start with a key frame (flagged by `couldBacktrack`) 27314 if (this.couldBacktrack && !this.fragPrevious && frag && frag.sn !== 'initSegment' && this.fragmentTracker.getState(frag) !== FragmentState.OK) { 27315 var _this$backtrackFragme; 27316 const backtrackSn = ((_this$backtrackFragme = this.backtrackFragment) != null ? _this$backtrackFragme : frag).sn; 27317 const fragIdx = backtrackSn - levelDetails.startSN; 27318 const backtrackFrag = levelDetails.fragments[fragIdx - 1]; 27319 if (backtrackFrag && frag.cc === backtrackFrag.cc) { 27320 frag = backtrackFrag; 27321 this.fragmentTracker.removeFragment(backtrackFrag); 27322 } 27323 } else if (this.backtrackFragment && bufferInfo.len) { 27324 this.backtrackFragment = null; 27325 } 27326 // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags 27327 if (frag && this.isLoopLoading(frag, targetBufferTime)) { 27328 const gapStart = frag.gap; 27329 if (!gapStart) { 27330 // Cleanup the fragment tracker before trying to find the next unbuffered fragment 27331 const type = this.audioOnly && !this.altAudio ? ElementaryStreamTypes.AUDIO : ElementaryStreamTypes.VIDEO; 27332 const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; 27333 if (mediaBuffer) { 27334 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); 27335 } 27336 } 27337 frag = this.getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); 27338 } 27339 if (!frag) { 27340 return; 27341 } 27342 if (frag.initSegment && !frag.initSegment.data && !this.bitrateTest) { 27343 frag = frag.initSegment; 27344 } 27345 this.loadFragment(frag, levelInfo, targetBufferTime); 27346 } 27347 loadFragment(frag, level, targetBufferTime) { 27348 // Check if fragment is not loaded 27349 const fragState = this.fragmentTracker.getState(frag); 27350 this.fragCurrent = frag; 27351 if (fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) {
27352 if (frag.sn === 'initSegment') { 27353 this._loadInitSegment(frag, level); 27354 } else if (this.bitrateTest) { 27355 this.log(`Fragment ${frag.sn} of level ${frag.level} is being downloaded to test bitrate and will not be buffered`); 27356 this._loadBitrateTestFrag(frag, level); 27357 } else { 27358 this.startFragRequested = true; 27359 super.loadFragment(frag, level, targetBufferTime); 27360 } 27361 } else { 27362 this.clearTrackerIfNeeded(frag); 27363 } 27364 } 27365 getBufferedFrag(position) { 27366 return this.fragmentTracker.getBufferedFrag(position, PlaylistLevelType.MAIN); 27367 } 27368 followingBufferedFrag(frag) { 27369 if (frag) { 27370 // try to get range of next fragment (500ms after this range) 27371 return this.getBufferedFrag(frag.end + 0.5); 27372 } 27373 return null; 27374 } 27375 27376 /* 27377 on immediate level switch : 27378 - pause playback if playing 27379 - cancel any pending load request 27380 - and trigger a buffer flush 27381 */ 27382 immediateLevelSwitch() { 27383 this.abortCurrentFrag(); 27384 this.flushMainBuffer(0, Number.POSITIVE_INFINITY); 27385 } 27386 27387 /** 27388 * try to switch ASAP without breaking video playback: 27389 * in order to ensure smooth but quick level switching, 27390 * we need to find the next flushable buffer range 27391 * we should take into account new segment fetch time 27392 */ 27393 nextLevelSwitch() { 27394 const { 27395 levels, 27396 media 27397 } = this; 27398 // ensure that media is defined and that metadata are available (to retrieve currentTime) 27399 if (media != null && media.readyState) { 27400 let fetchdelay; 27401 const fragPlayingCurrent = this.getAppendedFrag(media.currentTime); 27402 if (fragPlayingCurrent && fragPlayingCurrent.start > 1) { 27403 // flush buffer preceding current fragment (flush until current fragment start offset) 27404 // minus 1s to avoid video freezing, that could happen if we flush ke
27404yframe of current video ... 27405 this.flushMainBuffer(0, fragPlayingCurrent.start - 1); 27406 } 27407 const levelDetails = this.getLevelDetails(); 27408 if (levelDetails != null && levelDetails.live) { 27409 const bufferInfo = this.getMainFwdBufferInfo(); 27410 // Do not flush in live stream with low buffer 27411 if (!bufferInfo || bufferInfo.len < levelDetails.targetduration * 2) { 27412 return; 27413 } 27414 } 27415 if (!media.paused && levels) { 27416 // add a safety delay of 1s 27417 const nextLevelId = this.hls.nextLoadLevel; 27418 const nextLevel = levels[nextLevelId]; 27419 const fragLastKbps = this.fragLastKbps; 27420 if (fragLastKbps && this.fragCurrent) { 27421 fetchdelay = this.fragCurrent.duration * nextLevel.maxBitrate / (1000 * fragLastKbps) + 1; 27422 } else { 27423 fetchdelay = 0; 27424 } 27425 } else { 27426 fetchdelay = 0; 27427 } 27428 // this.log('fetchdelay:'+fetchdelay); 27429 // find buffer range that will be reached once new fragment will be fetched 27430 const bufferedFrag = this.getBufferedFrag(media.currentTime + fetchdelay); 27431 if (bufferedFrag) { 27432 // we can flush buffer range following this one without stalling playback 27433 const nextBufferedFrag = this.followingBufferedFrag(bufferedFrag); 27434 if (nextBufferedFrag) { 27435 // if we are here, we can also cancel any loading/demuxing in progress, as they are useless 27436 this.abortCurrentFrag(); 27437 // start flush position is in next buffered frag. Leave some padding for non-independent segments and smoother playback. 27438 const maxStart = nextBufferedFrag.maxStartPTS ? nextBufferedFrag.maxStartPTS : nextBufferedFrag.start; 27439 const fragDuration = nextBufferedFrag.duration; 27440 const startPts = Math.max(bufferedFrag.end, maxStart + Math.min(Math.max(fragDuration - this.config.maxFragLookUpTolerance, fragDuration * (this.couldBacktrack ? 0.5 : 0.125)), fragDuration * (this.couldBacktrack ? 0.75 : 0.25))); 27441 this.flushMainBuffer(startPts, Number.POSITIVE_INFINITY); 27442 } 27443 } 27444 } 27445 } 27446 abortCurrentFrag() { 27447 const fragCurrent = this.fragCurrent; 27448 this.fragCurrent = null; 27449 this.backtrackFragment = null; 27450 if (fragCurrent) { 27451 fragCurrent.abortRequests(); 27452 this.fragmentTracker.removeFragment(fragCurrent); 27453 } 27454 switch (this.state) { 27455 case State.KEY_LOADING: 27456 case State.FRAG_LOADING: 27457 case State.FRAG_LOADING_WAITING_RETRY: 27458 case State.PARSING: 27459 case State.PARSED: 27460 this.state = State.IDLE; 27461 break; 27462 } 27463 this.nextLoadPosition = this.getLoadPosition(); 27464 } 27465 flushMainBuffer(startOffset, endOffset) { 27466 super.flushMainBuffer(startOffset, endOffset, this.altAudio ? 'video' : null); 27467 } 27468 onMediaAttached(event, data) { 27469 super.onMediaAttached(event, data); 27470 const media = data.media; 27471 this.onvplaying = this.onMediaPlaying.bind(this); 27472 this.onvseeked = this.onMediaSeeked.bind(this); 27473 media.addEventListener('playing', this.onvplaying); 27474 media.addEventListener('seeked', this.onvseeked); 27475 this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls); 27476 } 27477 onMediaDetaching() { 27478 const { 27479 media 27480 } = this; 27481 if (media && this.onvplaying && this.onvseeked) { 27482 media.removeEventListener('playing', this.onvplaying); 27483 media.removeEventListener('seeked', this.onvseeked); 27484 this.onvplaying = this.onvseeked = null; 27485 this.videoBuffer = null; 27486 } 27487 this.fragPlaying = null; 27488 if (this.gapController) { 27489 this.gapController.destroy(); 27490 this.gapController = null; 27491 } 27492 super.onMediaDetaching(); 27493 } 27494 onMediaPlaying() { 27495 // tick to speed up FRAG_CHANGED triggering 27496 this.tick(); 27497 } 27498 onMediaSeeked() { 27499 const media = this.media; 27500 const currentTime = media ? media.currentTime : null; 27501 if (isFiniteNumber(currentTime)) { 27502 this.log(`Media seeked to ${currentTime.toFixed(3)}`); 27503 } 27504 27505 // If seeked was issued before buffer was appended do not tick immediately 27506 const bufferInfo = this.getMainFwdBufferInfo(); 27507 if (bufferInfo === null || bufferInfo.len === 0) { 27508 this.warn(`Main forward buffer length on "seeked" event ${bufferInfo ? bufferInfo.len : 'empty'})`); 27509 return; 27510 } 27511 27512 // tick to speed up FRAG_CHANGED triggering 27513 this.tick(); 27514 } 27515 onManifestLoading() { 27516 // reset buffer on manifest loading 27517 this.log('Trigger BUFFER_RESET'); 27518 this.hls.trigger(Events.BUFFER_RESET, undefined); 27519 this.fragmentTracker.removeAllFragments(); 27520 this.couldBacktrack = false;
vendor: 5,039 bytes, lines 27521-27644
27521 this.startPosition = this.lastCurrentTime = this.fragLastKbps = 0; 27522 this.levels = this.fragPlaying = this.backtrackFragment = this.levelLastLoaded = null; 27523 this.altAudio = this.audioOnly = this.startFragRequested = false; 27524 } 27525 onManifestParsed(event, data) { 27526 // detect if we have different kind of audio codecs used amongst playlists 27527 let aac = false; 27528 let heaac = false; 27529 data.levels.forEach(level => { 27530 const codec = level.audioCodec; 27531 if (codec) { 27532 aac = aac || codec.indexOf('mp4a.40.2') !== -1; 27533 heaac = heaac || codec.indexOf('mp4a.40.5') !== -1; 27534 } 27535 }); 27536 this.audioCodecSwitch = aac && heaac && !changeTypeSupported(); 27537 if (this.audioCodecSwitch) { 27538 this.log('Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC'); 27539 } 27540 this.levels = data.levels; 27541 this.startFragRequested = false; 27542 } 27543 onLevelLoading(event, data) { 27544 const { 27545 levels 27546 } = this; 27547 if (!levels || this.state !== State.IDLE) { 27548 return; 27549 } 27550 const level = levels[data.level]; 27551 if (!level.details || level.details.live && this.levelLastLoaded !== level || this.waitForCdnTuneIn(level.details)) { 27552 this.state = State.WAITING_LEVEL; 27553 } 27554 } 27555 onLevelLoaded(event, data) { 27556 var _curLevel$details; 27557 const { 27558 levels 27559 } = this; 27560 const newLevelId = data.level; 27561 const newDetails = data.details; 27562 const duration = newDetails.totalduration; 27563 if (!levels) { 27564 this.warn(`Levels were reset while loading level ${newLevelId}`); 27565 return; 27566 } 27567 this.log(`Level ${newLevelId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''}, cc [${newDetails.startCC}, ${newDetails.endCC}] duration:${duration}`); 27568 const curLevel = levels[newLevelId]; 27569 const fragCurrent = this.fragCurrent; 27570 if (fragCurrent && (this.state === State.FRAG_LOADING || this.state === State.FRAG_LOADING_WAITING_RETRY)) { 27571 if (fragCurrent.level !== data.level && fragCurrent.loader) { 27572 this.abortCurrentFrag(); 27573 } 27574 } 27575 let sliding = 0; 27576 if (newDetails.live || (_curLevel$details = curLevel.details) != null && _curLevel$details.live) { 27577 var _this$levelLastLoaded; 27578 this.checkLiveUpdate(newDetails); 27579 if (newDetails.deltaUpdateFailed) { 27580 return; 27581 } 27582 sliding = this.alignPlaylists(newDetails, curLevel.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 27583 } 27584 // override level info 27585 curLevel.details = newDetails; 27586 this.levelLastLoaded = curLevel; 27587 this.hls.trigger(Events.LEVEL_UPDATED, { 27588 details: newDetails, 27589 level: newLevelId 27590 }); 27591 27592 // only switch back to IDLE state if we were waiting for level to start downloading a new fragment 27593 if (this.state === State.WAITING_LEVEL) { 27594 if (this.waitForCdnTuneIn(newDetails)) { 27595 // Wait for Low-Latency CDN Tune-in 27596 return; 27597 } 27598 this.state = State.IDLE; 27599 } 27600 if (!this.startFragRequested) { 27601 this.setStartPosition(newDetails, sliding); 27602 } else if (newDetails.live) { 27603 this.synchronizeToLiveEdge(newDetails); 27604 } 27605 27606 // trigger handler right now 27607 this.tick(); 27608 } 27609 _handleFragmentLoadProgress(data) { 27610 var _frag$initSegment; 27611 const { 27612 frag, 27613 part, 27614 payload 27615 } = data; 27616 const { 27617 levels 27618 } = this; 27619 if (!levels) { 27620 this.warn(`Levels were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); 27621 return; 27622 } 27623 const currentLevel = levels[frag.level]; 27624 const details = currentLevel.details; 27625 if (!details) { 27626 this.warn(`Dropping fragment ${frag.sn} of level ${frag.level} after level details were reset`); 27627 this.fragmentTracker.removeFragment(frag); 27628 return; 27629 } 27630 const videoCodec = currentLevel.videoCodec; 27631 27632 // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) 27633 const accurateTimeOffset = details.PTSKnown || !details.live; 27634 const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; 27635 const audioCodec = this._getAudioCodec(currentLevel); 27636 27637 // transmux the MPEG-TS data to ISO-BMFF segments 27638 // this.log(`Transmuxing ${frag.sn} of [${details.startSN} ,${details.endSN}],level ${frag.level}, cc ${frag.cc}`); 27639 const transmuxer = this.transmuxer = this.transmuxer || new TransmuxerInterface(this.hls, PlaylistLevelType.MAIN, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); 27640 const partIndex = part ? part.index : -1; 27641 const partial = partIndex !== -1; 27642 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); 27643 const initPTS = this.initPTS[frag.cc]; 27644 transmuxer.push(payload, initSegmentData, audioCodec, videoCodec, frag, part, details.totalduration, accurateTimeOff
27644set, chunkMeta, initPTS); 27645 } 27646 onAudioTrackSwitching(event, data) { 27647 // if any URL found on new audio track, it is an alternate audio track 27648 const fromAltAudio = this.altAudio; 27649 const altAudio = !!data.url; 27650 // if we switch on main audio, ensure that main fragment scheduling is synced with media.buffered 27651 // don't do anything if we switch to alt audio: audio stream controller is handling it. 27652 // we will just have to change buffer scheduling on audioTrackSwitched 27653 if (!altAudio) { 27654 if (this.mediaBuffer !== this.media) { 27655 this.log('Switching on main audio, use media.buffered to schedule main fragment loading'); 27656 this.mediaBuffer = this.media; 27657 const fragCurrent = this.fragCurrent; 27658 // we need to refill audio buffer from main: cancel any frag loading to speed up audio switch 27659 if (fragCurrent) { 27660 this.log('Switching to main audio track, cancel main fragment load'); 27661 fragCurrent.abortRequests(); 27662 this.fragmentTracker.removeFragment(fragCurrent); 27663 } 27664 // destroy transmuxer to force init segment generation (following audio switch) 27665 this.resetTransmuxer(); 27666 // switch to IDLE state to load new fragment 27667 this.resetLoadingState(); 27668 } else if (this.audioOnly) { 27669 // Reset audio transmuxer so when switching back to main audio we're not still appending where we left off 27670 this.resetTransmuxer(); 27671 } 27672 const hls = this.hls; 27673 // If switching from alt to main audio, flush all audio and trigger track switched 27674 if (fromAltAudio) { 27675 hls.trigger(Events.BUFFER_FLUSHING, { 27676 startOffset: 0, 27677 endOffset: Number.POSITIVE_INFINITY, 27678 type: null 27679 }); 27680 this.fragmentTracker.removeAllFragments(); 27681 } 27682 hls.trigger(Events.AUDIO_TRACK_SWITCHED, data); 27683 } 27684 } 27685 onAudioTrackSwitched(event, data) { 27686 const trackId = data.id; 27687 const altAudio = !!this.hls.audioTracks[trackId].url; 27688 if (altAudio) { 27689 const videoBuffer = this.videoBuffer; 27690 // if we switched on alternate audio, ensure that main fragment scheduling is synced with video sourcebuffer buffered 27691 if (videoBuffer && this.mediaBuffer !== videoBuffer) { 27692 this.log('Switching on alternate audio, use video.buffered to schedule main fragment loading'); 27693 this.mediaBuffer = videoBuffer; 27694 } 27695 } 27696 this.altAudio = altAudio; 27697 this.tick(); 27698 } 27699 onBufferCreated(event, data) { 27700 const tracks = data.tracks; 27701 let mediaTrack; 27702 let name; 27703 let alternate = false; 27704 for (const type in tracks) { 27705 const track = tracks[type]; 27706 if (track.id === 'main') { 27707 name = type; 27708 mediaTrack = track; 27709 // keep video source buffer reference 27710 if (type === 'video') { 27711 const videoTrack = tracks[type]; 27712 if (videoTrack) { 27713 this.videoBuffer = videoTrack.buffer; 27714 } 27715 } 27716 } else { 27717 alternate = true; 27718 } 27719 } 27720 if (alternate && mediaTrack) { 27721 this.log(`Alternate track found, use ${name}.buffered to schedule main fragment loading`); 27722 this.mediaBuffer = mediaTrack.buffer; 27723 } else { 27724 this.mediaBuffer = this.media; 27725 } 27726 } 27727 onFragBuffered(event, data) { 27728 const { 27729 frag, 27730 part 27731 } = data; 27732 if (frag && frag.type !== PlaylistLevelType.MAIN) { 27733 return; 27734 } 27735 if (this.fragContextChanged(frag)) { 27736 // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion 27737 // Avoid setting state back to IDLE, since that will interfere with a level switch 27738 this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}`); 27739 if (this.state === State.PARSED) { 27740 this.state = State.IDLE; 27741 } 27742 return; 27743 } 27744 const stats = part ? part.stats : frag.stats; 27745 this.fragLastKbps = Math.round(8 * stats.total / (stats.buffering.end - stats.loading.first)); 27746 if (frag.sn !== 'initSegment') { 27747 this.fragPrevious = frag; 27748 } 27749 this.fragBufferedComplete(frag, part); 27750 } 27751 onError(event, data) { 27752 var _data$context; 27753 if (data.fatal) { 27754 this.state = State.ERROR; 27755 return; 27756 } 27757 switch (data.details) { 27758 case ErrorDetails.FRAG_GAP: 27759 case ErrorDetails.FRAG_PARSING_ERROR: 27760 case ErrorDetails.FRAG_DECRYPT_ERROR: 27761 case ErrorDetails.FRAG_LOAD_ERROR: 27762 case ErrorDetails.FRAG_LOAD_TIMEOUT: 27763 case ErrorDetails.KEY_LOAD_ERROR: 27764 case ErrorDetails.KEY_LOAD_TIMEOUT: 27765 this.onFragmentOrKeyLoadError(PlaylistLevelType.MAIN, data); 27766 break; 27767 case ErrorDetails.LEVEL_LOAD_ERROR: 27768 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 27769 case ErrorDetails.LEVEL_PARSING_ERROR: 27770 // in case of non fatal error while loading level, if level controller is not retrying to load level, switch back to IDLE 27771 if (!data.levelRetry && this.state === State.WAITING_LEVEL && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.LEVEL) { 27772 this.state = State.IDLE; 27773 } 27774 break; 27775 case ErrorDetails.BUFFER_APPEND_ERROR: 27776 case ErrorDetails.BUFFER_FULL_ERROR: 27777 if (!data.parent || data.parent !== 'main') { 27778 return; 27779 } 27780 if (data.details === ErrorDetails.BUFFER_APPEND_ERROR) { 27781 this.resetLoadingState(); 27782 return; 27783 } 27784 if (this.reduceLengthAndFlushBuffer(data)) { 27785 this.flushMainBuffer(0, Number.POSITIVE_INFINITY); 27786 } 27787 break; 27788 case ErrorDetails.INTERNAL_EXCEPTION: 27789 this.recoverWorkerError(data); 27790 break; 27791 } 27792 } 27793 27794 // Checks the health of the buffer and attempts to resolve playback stalls. 27795 checkBuffer() { 27796 const { 27797 media, 27798 gapController 27799 } = this; 27800 if (!media || !gapController || !media.readyState) { 27801 // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0) 27802 return; 27803 } 27804 if (this.loadedmetadata || !BufferHelper.getBuffered(media).length) { 27805 // Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers 27806 const activeFrag = this.state !== State.IDLE ? this.fragCurrent : null; 27807 gapController.poll(this.lastCurrentTime, activeFrag); 27808 } 27809 this.lastCurrentTime = media.currentTime; 27810 } 27811 onFragLoadEmergencyAborted() { 27812 this.state = State.IDLE; 27813 // if loadedmetadata is not set, it means that we are emergency switch down on first frag 27814 // in that case, reset startFragRequested flag 27815 if (!this.loadedmetadata) { 27816 this.startFragRequested = false;
27817 this.nextLoadPosition = this.startPosition; 27818 } 27819 this.tickImmediate(); 27820 } 27821 onBufferFlushed(event, { 27822 type 27823 }) { 27824 if (type !== ElementaryStreamTypes.AUDIO || this.audioOnly && !this.altAudio) { 27825 const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; 27826 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); 27827 this.tick(); 27828 } 27829 } 27830 onLevelsUpdated(event, data) { 27831 if (this.level > -1 && this.fragCurrent) { 27832 this.level = this.fragCurrent.level; 27833 } 27834 this.levels = data.levels; 27835 } 27836 swapAudioCodec() { 27837 this.audioCodecSwap = !this.audioCodecSwap; 27838 } 27839 27840 /** 27841 * Seeks to the set startPosition if not equal to the mediaElement's current time. 27842 */ 27843 seekToStartPos() { 27844 const { 27845 media 27846 } = this; 27847 if (!media) { 27848 return; 27849 } 27850 const currentTime = media.currentTime; 27851 let startPosition = this.startPosition; 27852 // only adjust currentTime if different from startPosition or if startPosition not buffered 27853 // at that stage, there should be only one buffered range, as we reach that code after first fragment has been buffered 27854 if (startPosition >= 0 && currentTime < startPosition) { 27855 if (media.seeking) { 27856 this.log(`could not seek to ${startPosition}, already seeking at ${currentTime}`); 27857 return; 27858 } 27859 const buffered = BufferHelper.getBuffered(media); 27860 const bufferStart = buffered.length ? buffered.start(0) : 0; 27861 const delta = bufferStart - startPosition; 27862 if (delta > 0 && (delta < this.config.maxBufferHole || delta < this.config.maxFragLookUpTolerance)) { 27863 this.log(`adjusting start position by ${delta} to match buffer start`); 27864 startPosition += delta; 27865 this.startPosition = startPosition; 27866 } 27867 this.log(`seek to target start position ${startPosition} from current time ${currentTime}`); 27868 media.currentTime = startPosition; 27869 } 27870 } 27871 _getAudioCodec(currentLevel) { 27872 let audioCodec = this.config.defaultAudioCodec || currentLevel.audioCodec; 27873 if (this.audioCodecSwap && audioCodec) { 27874 this.log('Swapping audio codec'); 27875 if (audioCodec.indexOf('mp4a.40.5') !== -1) { 27876 audioCodec = 'mp4a.40.2'; 27877 } else { 27878 audioCodec = 'mp4a.40.5'; 27879 } 27880 } 27881 return audioCodec; 27882 } 27883 _loadBitrateTestFrag(frag, level) { 27884 frag.bitrateTest = true; 27885 this._doFragLoad(frag, level).then(data => { 27886 const { 27887 hls 27888 } = this; 27889 if (!data || this.fragContextChanged(frag)) { 27890 return; 27891 } 27892 level.fragmentError = 0; 27893 this.state = State.IDLE; 27894 this.startFragRequested = false; 27895 this.bitrateTest = false; 27896 const stats = frag.stats; 27897 // Bitrate tests fragments are neither parsed nor buffered 27898 stats.parsing.start = stats.parsing.end = stats.buffering.start = stats.buffering.end = self.performance.now(); 27899 hls.trigger(Events.FRAG_LOADED, data); 27900 frag.bitrateTest = false; 27901 }); 27902 } 27903 _handleTransmuxComplete(transmuxResult) { 27904 var _id3$samples; 27905 const id = 'main'; 27906 const { 27907 hls 27908 } = this; 27909 const { 27910 remuxResult, 27911 chunkMeta 27912 } = transmuxResult; 27913 const context = this.getCurrentContext(chunkMeta); 27914 if (!context) { 27915 this.resetWhenMissingContext(chunkMeta); 27916 return; 27917 } 27918 const { 27919 frag, 27920 part, 27921 level 27922 } = context; 27923 const { 27924 video, 27925 text, 27926 id3, 27927 initSegment 27928 } = remuxResult; 27929 const { 27930 details 27931 } = level; 27932 // The audio-stream-controller handles audio buffering if Hls.js is playing an alternate audio track 27933 const audio = this.altAudio ? undefined : remuxResult.audio; 27934 27935 // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. 27936 // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. 27937 if (this.fragContextChanged(frag)) { 27938 this.fragmentTracker.removeFragment(frag); 27939 return; 27940 } 27941 this.state = State.PARSING; 27942 if (initSegment) { 27943 if (initSegment != null && initSegment.tracks) { 27944 const mapFragment = frag.initSegment || frag; 27945 this._bufferInitSegment(level, initSegment.tracks, mapFragment, chunkMeta); 27946 hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { 27947 frag: mapFragment, 27948 id, 27949 tracks: initSegment.tracks 27950 }); 27951 } 27952 27953 // This would be nice if Number.isFinite acted as a typeguard, but it doesn't. See: https://github.com/Microsoft/TypeScript/issues/10038 27954 const initPTS = initSegment.initPTS; 27955 const timescale = initSegment.timescale; 27956 if (isFiniteNumber(initPTS)) { 27957 this.initPTS[frag.cc] = { 27958 baseTime: initPTS, 27959 timescale 27960 }; 27961 hls.trigger(Events.INIT_PTS_FOUND, { 27962 frag, 27963 id, 27964 initPTS, 27965 timescale 27966 }); 27967 } 27968 } 27969 27970 // Avoid buffering if backtracking this fragment 27971 if (video && details && frag.sn !== 'initSegment') { 27972 const prevFrag = details.fragments[frag.sn - 1 - details.startSN]; 27973 const isFirstFragment = frag.sn === details.startSN; 27974 const isFirstInDiscontinuity = !prevFrag || frag.cc > prevFrag.cc; 27975 if (remuxResult.independent !== false) { 27976 const { 27977 startPTS, 27978 endPTS, 27979 startDTS, 27980 endDTS 27981 } = video; 27982 if (part) { 27983 part.elementaryStreams[video.type] = { 27984 startPTS, 27985 endPTS, 27986 startDTS, 27987 endDTS 27988 }; 27989 } else { 27990 if (video.firstKeyFrame && video.independent && chunkMeta.id === 1 && !isFirstInDiscontinuity) { 27991 this.couldBacktrack = true; 27992 } 27993 if (video.dropped && video.independent) { 27994 // Backtrack if dropped frames create a gap after currentTime 27995 27996 const bufferInfo = this.getMainFwdBufferInfo(); 27997 const targetBufferTime = (bufferInfo ? bufferInfo.end : this.getLoadPosition()) + this.config.maxBufferHole; 27998 const startTime = video.firstKeyFramePTS ? video.firstKeyFramePTS : startPTS; 27999 if (!isFirstFragment && targetBufferTime < startTime - this.config.maxBufferHole && !isFirstInDiscontinuity) { 28000 this.backtrack(frag); 28001 return; 28002 } else if (isFirstInDiscontinuity) { 28003 // Mark segment with a gap to avoid loop loading 28004 frag.gap = true; 28005 } 28006 // Set video stream start to fragment start so that truncated samples do not distort the timeline, and mark it partial 28007 frag.setElementaryStreamInfo(video.type, frag.start, endPTS, frag.start, endDTS, true); 28008 } else if (isFirstFragment && startPTS > MAX_START_GAP_JUMP) { 28009 // Mark segment with a gap to skip large start gap 28010 frag.gap = true; 28011 } 28012 } 28013 frag.setElementaryStreamInfo(video.type, startPTS, endPTS, startDTS, endDTS); 28014 if (this.backtrackFragment) { 28015 this.backtrackFragment = frag; 28016 } 28017 this.bufferFragmentData(video, frag, part, chunkMeta, isFirstFragment || isFirstInDiscontinuity); 28018 } else if (isFirstFragment || isFirstInDiscontinuity) { 28019 // Mark segment with a gap to avoid loop loading 28020 frag.gap = true; 28021 } else { 28022 this.backtrack(frag); 28023 return; 28024 } 28025 } 28026 if (audio) { 28027 const { 28028 startPTS, 28029 endPTS, 28030 startDTS, 28031 endDTS 28032 } = audio; 28033 if (part) { 28034 part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { 28035 startPTS, 28036 endPTS, 28037 startDTS, 28038 endDTS 28039 }; 28040 } 28041 frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); 28042 this.bufferFragmentData(audio, frag, part, chunkMeta); 28043 } 28044 if (details && id3 != null && (_id3$samples = id3.samples) != null && _id3$samples.length) { 28045 const emittedID3 = { 28046 id, 28047 frag, 28048 details, 28049 samples: id3.samples 28050 }; 28051 hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); 28052 } 28053 if (details && text) { 28054 const emittedText = { 28055 id, 28056 frag, 28057 details, 28058 samples: text.samples 28059 }; 28060 hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); 28061 } 28062 } 28063 _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { 28064 if (this.state !== State.PARSING) { 28065 return; 28066 } 28067 this.audioOnly = !!tracks.audio && !tracks.video; 28068 28069 // if audio track is expected to come from audio stream controller, discard any coming from main 28070 if (this.altAudio && !this.audioOnly) { 28071 delete tracks.audio; 28072 } 28073 // include levelCodec in audio and video tracks 28074 const { 28075 audio, 28076 video, 28077 audiovideo 28078 } = tracks; 28079 if (audio) { 28080 let audioCodec = currentLevel.audioCodec; 28081 const ua = navigator.userAgent.toLowerCase(); 28082 if (this.audioCodecSwitch) { 28083 if (audioCodec) { 28084 if (audioCodec.indexOf('mp4a.40.5') !== -1) { 28085 audioCodec = 'mp4a.40.2'; 28086 } else { 28087 audioCodec = 'mp4a.40.5'; 28088 } 28089 } 28090 // In the case that AAC and HE-AAC audio codecs are signalled
28090in manifest, 28091 // force HE-AAC, as it seems that most browsers prefers it. 28092 // don't force HE-AAC if mono stream, or in Firefox 28093 const audioMetadata = audio.metadata; 28094 if (audioMetadata && 'channelCount' in audioMetadata && (audioMetadata.channelCount || 1) !== 1 && ua.indexOf('firefox') === -1) { 28095 audioCodec = 'mp4a.40.5'; 28096 } 28097 } 28098 // HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise 28099 if (audioCodec && audioCodec.indexOf('mp4a.40.5') !== -1 && ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') { 28100 // Exclude mpeg audio 28101 audioCodec = 'mp4a.40.2'; 28102 this.log(`Android: force audio codec to ${audioCodec}`); 28103 } 28104 if (currentLevel.audioCodec && currentLevel.audioCodec !== audioCodec) { 28105 this.log(`Swapping manifest audio codec "${currentLevel.audioCodec}" for "${audioCodec}"`); 28106 } 28107 audio.levelCodec = audioCodec; 28108 audio.id = 'main'; 28109 this.log(`Init audio buffer, container:${audio.container}, codecs[selected/level/parsed]=[${audioCodec || ''}/${currentLevel.audioCodec || ''}/${audio.codec}]`); 28110 } 28111 if (video) { 28112 video.levelCodec = currentLevel.videoCodec; 28113 video.id = 'main'; 28114 this.log(`Init video buffer, container:${video.container}, codecs[level/parsed]=[${currentLevel.videoCodec || ''}/${video.codec}]`); 28115 } 28116 if (audiovideo) { 28117 this.log(`Init audiovideo buffer, container:${audiovideo.container}, codecs[level/parsed]=[${currentLevel.codecs}/${audiovideo.codec}]`); 28118 } 28119 this.hls.trigger(Events.BUFFER_CODECS, tracks); 28120 // loop through tracks that are going to be provided to bufferController
28121 Object.keys(tracks).forEach(trackName => { 28122 const track = tracks[trackName]; 28123 const initSegment = track.initSegment; 28124 if (initSegment != null && initSegment.byteLength) { 28125 this.hls.trigger(Events.BUFFER_APPENDING, { 28126 type: trackName, 28127 data: initSegment, 28128 frag, 28129 part: null, 28130 chunkMeta, 28131 parent: frag.type 28132 }); 28133 } 28134 }); 28135 // trigger handler right now 28136 this.tickImmediate(); 28137 } 28138 getMainFwdBufferInfo() { 28139 return this.getFwdBufferInfo(this.mediaBuffer ? this.mediaBuffer : this.media, PlaylistLevelType.MAIN); 28140 } 28141 backtrack(frag) { 28142 this.couldBacktrack = true; 28143 // Causes findFragments to backtrack through fragments to find the keyframe 28144 this.backtrackFragment = frag; 28145 this.resetTransmuxer(); 28146 this.flushBufferGap(frag); 28147 this.fragmentTracker.removeFragment(frag); 28148 this.fragPrevious = null; 28149 this.nextLoadPosition = frag.start; 28150 this.state = State.IDLE; 28151 } 28152 checkFragmentChanged() { 28153 const video = this.media; 28154 let fragPlayingCurrent = null; 28155 if (video && video.readyState > 1 && video.seeking === false) { 28156 const currentTime = video.currentTime; 28157 /* if video element is in seeked state, currentTime can only increase. 28158 (assuming that playback rate is positive ...) 28159 As sometimes currentTime jumps back to zero after a 28160 media decode error, check this, to avoid seeking back to 28161 wrong position after a media decode error 28162 */ 28163 28164 if (BufferHelper.isBuffered(video, currentTime)) { 28165 fragPlayingCurrent = this.getAppendedFrag(currentTime); 28166 } else if (BufferHelper.isBuffered(video, currentTime + 0.1)) { 28167 /* ensure that FRAG_CHANGED event is triggered at startup, 28168 when first video frame is displayed and playback is paused. 28169 add a tolerance of 100ms, in case current position is not buffered, 28170 check if current pos+100ms is buffered and use that buffer range 28171 for FRAG_CHANGED event reporting */ 28172 fragPlayingCurrent = this.getAppendedFrag(currentTime + 0.1); 28173 } 28174 if (fragPlayingCurrent) { 28175 this.backtrackFragment = null; 28176 const fragPlaying = this.fragPlaying; 28177 const fragCurrentLevel = fragPlayingCurrent.level; 28178 if (!fragPlaying || fragPlayingCurrent.sn !== fragPlaying.sn || fragPlaying.level !== fragCurrentLevel) { 28179 this.fragPlaying = fragPlayingCurrent; 28180 this.hls.trigger(Events.FRAG_CHANGED, { 28181 frag: fragPlayingCurrent 28182 }); 28183 if (!fragPlaying || fragPlaying.level !== fragCurrentLevel) { 28184 this.hls.trigger(Events.LEVEL_SWITCHED, { 28185 level: fragCurrentLevel 28186 }); 28187 } 28188 } 28189 } 28190 } 28191 } 28192 get nextLevel() { 28193 const frag = this.nextBufferedFrag; 28194 if (frag) { 28195 return frag.level; 28196 } 28197 return -1; 28198 } 28199 get currentFrag() { 28200 const media = this.media; 28201 if (media) { 28202 return this.fragPlaying || this.getAppendedFrag(media.currentTime); 28203 } 28204 return null; 28205 } 28206 get currentProgramDateTime() { 28207 const media = this.media; 28208 if (media) { 28209 const currentTime = media.currentTime; 28210 const frag = this.currentFrag; 28211 if (frag && isFiniteNumber(currentTime) && isFiniteNumber(frag.programDateTime)) { 28212 const epocMs = frag.programDateTime + (currentTime - frag.start) * 1000; 28213 return new Date(epocMs); 28214 } 28215 } 28216 return null; 28217 } 28218 get currentLevel() { 28219 const frag = this.currentFrag; 28220 if (frag) { 28221 return frag.level; 28222 } 28223 return -1; 28224 } 28225 get nextBufferedFrag() { 28226 const frag = this.currentFrag; 28227 if (frag) { 28228 return this.followingBufferedFrag(frag); 28229 } 28230 return null; 28231 } 28232 get forceStartLoad() { 28233 return this._forceStartLoad; 28234 } 28235} 28236 28237/** 28238 * The `Hls` class is the core of the HLS.js library used to instantiate player instances. 28239 * @public 28240 */ 28241class Hls { 28242 /** 28243 * Get the video-dev/hls.js package version. 28244 */ 28245 static get version() { 28246 return "1.5.13"; 28247 } 28248 28249 /** 28250 * Check if the required MediaSource Extensions are available. 28251 */ 28252 static isMSESupported() { 28253 return isMSESupported(); 28254 } 28255 28256 /** 28257 * Check if MediaSource Extensions are available and isTypeSupp
28257orted checks pass for any baseline codecs. 28258 */ 28259 static isSupported() { 28260 return isSupported(); 28261 } 28262 28263 /** 28264 * Get the MediaSource global used for MSE playback (ManagedMediaSource, MediaSource, or WebKitMediaSource). 28265 */ 28266 static getMediaSource() { 28267 return getMediaSource(); 28268 } 28269 static get Events() { 28270 return Events; 28271 } 28272 static get ErrorTypes() { 28273 return ErrorTypes; 28274 } 28275 static get ErrorDetails() { 28276 return ErrorDetails; 28277 } 28278 28279 /** 28280 * Get the default configuration applied to new instances. 28281 */ 28282 static get DefaultConfig() { 28283 if (!Hls.defaultConfig) { 28284 return hlsDefaultConfig; 28285 } 28286 return Hls.defaultConfig; 28287 } 28288 28289 /** 28290 * Replace the default configuration applied to new instances. 28291 */ 28292 static set DefaultConfig(defaultConfig) { 28293 Hls.defaultConfig = defaultConfig; 28294 } 28295 28296 /** 28297 * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`. 28298 * @param userConfig - Configuration options applied over `Hls.DefaultConfig` 28299 */ 28300 constructor(userConfig = {}) { 28301 /** 28302 * The runtime configuration used by the player. At instantiation this is combination of `hls.userConfig` merged over `Hls.DefaultConfig`. 28303 */ 28304 this.config = void 0; 28305 /** 28306 * The configuration object provided on player instantiation. 28307 */ 28308 this.userConfig = void 0; 28309 this.coreComponents = void 0; 28310 this.networkControllers = void 0; 28311 this.started = false; 28312 this._emitter = new EventEmitter(); 28313 this._autoLevelCapping = -1; 28314 this._maxHdcpLevel = null; 28315 this.abrController = void 0; 28316 this.bufferController = void 0; 28317 this.capLevelController = void 0; 28318 this.latencyController = void 0; 28319 this.levelController = void 0; 28320 this.streamController = void 0; 28321 this.audioTrackController = void 0; 28322 this.subtitleTrackController = void 0; 28323 this.emeController = void 0; 28324 this.cmcdController = void 0; 28325 this._media = null; 28326 this.url = null; 28327 this.triggeringException = void 0; 28328 enableLogs(userConfig.debug || false, 'Hls instance'); 28329 const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig); 28330 this.userConfig = userConfig; 28331 if (config.progressive) { 28332 enableStreamingMode(config); 28333 } 28334 28335 // core controllers and network loaders 28336 const { 28337 abrController: ConfigAbrController, 28338 bufferController: ConfigBufferController, 28339 capLevelController: ConfigCapLevelController, 28340 errorController: ConfigErrorController, 28341 fpsController: ConfigFpsController 28342 } = config; 28343 const errorController = new ConfigErrorController(this); 28344 const abrController = this.abrController = new ConfigAbrController(this); 28345 const bufferController = this.bufferController = new ConfigBufferController(this); 28346 const capLevelController = this.capLevelController = new ConfigCapLevelController(this); 28347 const fpsController = new ConfigFpsController(this); 28348 const playListLoader = new PlaylistLoader(this); 28349 const id3TrackController = new ID3TrackController(this); 28350 const ConfigContentSteeringController = config.contentSteeringController; 28351 // ConentSteeringController is defined before LevelController to receive Multivariant Playlist events first 28352 const contentSteering = ConfigContentSteeringController ? new ConfigContentSteeringController(this) : null; 28353 const levelController = this.levelController = new LevelController(this, contentSteering); 28354 // FragmentTracker must be defined before StreamController because the order of event handling is important 28355 const fragmentTracker = new FragmentTracker(this); 28356 const keyLoader = new KeyLoader(this.config); 28357 const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader); 28358 28359 // Cap level controller uses streamController to flush the buffer 28360 capLevelController.setStreamController(streamController); 28361 // fpsController uses streamController to switch when frames are being dropped 28362 fpsController.setStreamController(streamController); 28363 const networkControllers = [playListLoader, levelController, streamController]; 28364 if (contentSteering) { 28365 networkControllers.splice(1, 0, contentSteering); 28366 } 28367 this.networkControllers = networkControllers; 28368 const coreComponents = [abrController, bufferController, capLevelController, fpsController, id3TrackController, fragmentTracker]; 28369 this.audioTrackController = this.createController(config.audioTrackController, networkControllers); 28370 const AudioStreamControllerClass = config.audioStreamController; 28371 if (AudioStreamControllerClass) { 28372 networkControllers.push(new AudioStreamControllerClass(this, fragmentTracker, keyLoader)); 28373 }
28374 // subtitleTrackController must be defined before subtitleStreamController because the order of event handling is important 28375 this.subtitleTrackController = this.createController(config.subtitleTrackController, networkControllers); 28376 const SubtitleStreamControllerClass = config.subtitleStreamController; 28377 if (SubtitleStreamControllerClass) { 28378 networkControllers.push(new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader)); 28379 } 28380 this.createController(config.timelineController, coreComponents); 28381 keyLoader.emeController = this.emeController = this.createController(config.emeController, coreComponents); 28382 this.cmcdController = this.createController(config.cmcdController, coreComponents); 28383 this.latencyController = this.createController(LatencyController, coreComponents); 28384 this.coreComponents = coreComponents; 28385 28386 // Error controller handles errors before and after all other controllers 28387 // This listener will be invoked after all other controllers error listeners 28388 networkControllers.push(errorController); 28389 const onErrorOut = errorController.onErrorOut; 28390 if (typeof onErrorOut === 'function') { 28391 this.on(Events.ERROR, onErrorOut, errorController); 28392 } 28393 } 28394 createController(ControllerClass, components) { 28395 if (ControllerClass) { 28396 const controllerInstance = new ControllerClass(this); 28397 if (components) { 28398 components.push(controllerInstance); 28399 } 28400 return controllerInstance; 28401 } 28402 return null; 28403 } 28404 28405 // Delegate the EventEmitter through the public API of Hls.js 28406 on(event, listener, context = this) { 28407 this._emitter.on(event, listener, context); 28408 } 28409 once(event, listener, context = this) { 28410 this._emitter.once(event, listener, context); 28411 } 28412 removeAllListeners(event) { 28413 this._emitter.removeAllListeners(event); 28414 } 28415 off(event, listener, context = this, once) { 28416 this._emitter.off(event, listener, context, once); 28417 } 28418 listeners(event) { 28419 return this._emitter.listeners(event); 28420 } 28421 emit(event, name, eventObject) { 28422 return this._emitter.emit(event, name, eventObject); 28423 } 28424 trigger(event, eventObject) { 28425 if (this.config.debug) { 28426 return this.emit(event, event, eventObject); 28427 } else { 28428 try { 28429 return this.emit(event, event, eventObject); 28430 } catch (error) { 28431 logger.error('An internal error happened while handling event ' + event + '. Error message: "' + error.message + '". Here is a stacktrace:', error); 28432 // Prevent recursion in error event handlers that throw #5497 28433 if (!this.triggeringException) { 28434 this.triggeringException = true; 28435 const fatal = event === Events.ERROR; 28436 this.trigger(Events.ERROR, { 28437 type: ErrorTypes.OTHER_ERROR, 28438 details: ErrorDetails.INTERNAL_EXCEPTION, 28439 fatal, 28440 event, 28441 error 28442 }); 28443 this.triggeringException = false; 28444 } 28445 } 28446 } 28447 return false; 28448 } 28449 listenerCount(event) { 28450 return this._emitter.listenerCount(event); 28451 } 28452 28453 /** 28454 * Dispose of the instance 28455 */ 28456 destroy() { 28457 logger.log('destroy'); 28458 this.trigger(Events.DESTROYING, undefined); 28459 this.detachMedia(); 28460 this.removeAllListeners(); 28461 this._autoLevelCapping = -1; 28462 this.url = null; 28463 this.networkControllers.forEach(component => component.destroy()); 28464 this.networkControllers.length = 0; 28465 this.coreComponents.forEach(component => component.destroy()); 28466 this.coreComponents.length = 0; 28467 // Remove any references that could be held in config options or callbacks 28468 const config = this.config; 28469 config.xhrSetup = config.fetchSetup = undefined; 28470 // @ts-ignore 28471 this.userConfig = null; 28472 } 28473 28474 /** 28475 * Attaches Hls.js to a media element 28476 */ 28477 attachMedia(media) { 28478 logger.log('attachMedia'); 28479 this._media = media; 28480 this.trigger(Events.MEDIA_ATTACHING, { 28481 media: media 28482 }); 28483 } 28484 28485 /** 28486 * Detach Hls.js from the media 28487 */ 28488 detachMedia() { 28489 logger.log('detachMedia'); 28490 this.trigger(Events.MEDIA_DETACHING, undefined); 28491 this._media = null; 28492 } 28493 28494 /** 28495 * Set the source URL. Can be relative or absolute. 28496 */ 28497 loadSource(url) { 28498 this.stopLoad(); 28499 const media = this.media; 28500 const loadedSource = this.url; 28501 const loadingSource = this.url = urlToolkitExports.buildAbsoluteURL(self.location.href, url, { 28502 alwaysNormalize: true 28503 }); 28504 this._autoLevelCapping = -1; 28505 this._maxHdcpLevel = null; 28506 logger.log(`loadSource:${loadingSource}`); 28507 if (media && loadedSource && (loadedSource !== loadingSource || this.bufferController.hasSourceTypes())) { 28508 this.detachMedia(); 28509 this.attachMedia(media); 28510 } 28511 // when attaching to a source URL, trigger a playlist load 28512 this.trigger(Events.MANIFEST_LOADING, { 28513 url: url 28514 }); 28515 } 28516 28517 /** 28518 * Start loading data from the stream source.
28519 * Depending on default config, client starts loading automatically when a source is set. 28520 * 28521 * @param startPosition - Set the start position to stream from. 28522 * Defaults to -1 (None: starts from earliest point) 28523 */ 28524 startLoad(startPosition = -1) { 28525 logger.log(`startLoad(${startPosition})`); 28526 this.started = true; 28527 this.networkControllers.forEach(controller => { 28528 controller.startLoad(startPosition); 28529 }); 28530 } 28531 28532 /** 28533 * Stop loading of any stream data. 28534 */ 28535 stopLoad() { 28536 logger.log('stopLoad'); 28537 this.started = false; 28538 this.networkControllers.forEach(controller => { 28539 controller.stopLoad(); 28540 }); 28541 } 28542 28543 /** 28544 * Resumes stream controller segment loading if previously started. 28545 */ 28546 resumeBuffering() { 28547 if (this.started) { 28548 this.networkControllers.forEach(controller => { 28549 if ('fragmentLoader' in controller) { 28550 controller.startLoad(-1); 28551 } 28552 }); 28553 } 28554 } 28555 28556 /** 28557 * Stops stream controller segment loading without changing 'started' state like stopLoad(). 28558 * This allows for media buffering to be paused without interupting playlist loading. 28559 */ 28560 pauseBuffering() { 28561 this.networkControllers.forEach(controller => { 28562 if ('fragmentLoader' in controller) { 28563 controller.stopLoad(); 28564 } 28565 }); 28566 } 28567 28568 /** 28569 * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) 28570 */ 28571 swapAudioCodec() { 28572 logger.log('swapAudioCodec'); 28573 this.streamController.swapAudioCodec(); 28574 } 28575 28576 /** 28577 * When the media-element fails, this allows to detach and then re-attach it 28578 * as one call (convenience method). 28579 * 28580 * Automatic recovery of media-errors by this process is configurable. 28581 */ 28582 recoverMediaError() { 28583 logger.log('recoverMediaError'); 28584 const media = this._media; 28585 this.detachMedia(); 28586 if (media) { 28587 this.attachMedia(media); 28588 } 28589 } 28590 removeLevel(levelIndex) { 28591 this.levelController.removeLevel(levelIndex); 28592 } 28593 28594 /** 28595 * @returns an array of levels (variants) sorted by HDCP-LEVEL, RESOLUTION (height), FRAME-RATE, CODECS, VIDEO-RANGE, and BANDWIDTH 28596 */ 28597 get levels() { 28598 const levels = this.levelController.levels; 28599 return levels ? levels : []; 28600 } 28601 28602 /** 28603 * Index of quality level (variant) currently played 28604 */ 28605 get currentLevel() { 28606 return this.streamController.currentLevel; 28607 } 28608 28609 /** 28610 * Set quality level index immediately. This will flush the current buffer to replace the quality asap. That means playback will interrupt at least shortly to re-buffer and re-sync eventually. Set to -1 for automatic level selection. 28611 */ 28612 set currentLevel(newLevel) { 28613 logger.log(`set currentLevel:${newLevel}`); 28614 this.levelController.manualLevel = newLevel; 28615 this.streamController.immediateLevelSwitch(); 28616 } 28617 28618 /** 28619 * Index of next quality level loaded as scheduled by stream controller. 28620 */ 28621 get nextLevel() { 28622 return this.streamController.nextLevel; 28623 } 28624 28625 /** 28626 * Set quality level index for next loaded data. 28627 * This will switch the video quality asap, without interrupting playback. 28628 * May abort current loading of data, and flush parts of buffer (outside currently played fragment region). 28629 * @param newLevel - Pass -1 for automatic level selection 28630 */ 28631 set nextLevel(newLevel) { 28632 logger.log(`set nextLevel:${newLevel}`); 28633 this.levelController.manualLevel = newLevel; 28634 this.streamController.nextLevelSwitch(); 28635 } 28636 28637 /** 28638 * Return the quality level of the currently or last (of none is loaded currently) segment 28639 */ 28640 get loadLevel() { 28641 return this.levelController.level; 28642 } 28643 28644 /** 28645 * Set quality level index for next loaded data in a conservative way. 28646 * This will switch the quality without flushing, but interrupt current loading. 28647 * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer. 28648 * @param newLevel - Pass -1 for automatic level selection 28649 */ 28650 set loadLevel(newLevel) { 28651 logger.log(`set loadLevel:${newLevel}`); 28652 this.levelController.manualLevel = newLevel; 28653 } 28654 28655 /** 28656 * get next quality level loaded 28657 */ 28658 get nextLoadLevel() { 28659 return this.levelController.nextLoadLevel; 28660 } 28661 28662 /** 28663 * Set quality level of next loaded segment in a fully "non-destructive" way. 28664 * Same as `loadLevel` but will wait for next switch (until current loading is done). 28665 */ 28666 set nextLoadLevel(level) { 28667 this.levelController.nextLoadLevel = level; 28668 } 28669 28670 /** 28671 * Return "first level": like a default level, if not set, 28672 * falls back to index of first level referenced in manifest 28673 */ 28674 get firstLevel() { 28675 return Math.max(this.levelController.firstLevel, this.minAutoLevel); 28676 } 28677 28678 /** 28679 * Sets "first-level", see getter. 28680 */ 28681 set firstLevel(newLevel) { 28682 logger.log(`set firstLevel:${newLevel}`); 28683 this.levelController.firstLevel = newLevel; 28684 } 28685 28686 /** 28687 * Return the desired start level for the first fragment that will be loaded. 28688 * The default value of -1 indicates automatic start level selection. 28689 * Setting hls.nextAutoLevel without setting a startLevel will result in 28690 * the nextAutoLevel value being used for one fragment load. 28691 */ 28692 get startLevel() { 28693 const startLevel = this.levelController.startLevel; 28694 if (startLevel === -1 && this.abrController.forcedAutoLevel > -1) { 28695 return this.abrController.forcedAutoLevel; 28696 } 28697 return startLevel; 28698 } 28699 28700 /** 28701 * set start level (level of first fragment that will be played back) 28702 * if not overrided by user, first level appearing in manifest will be used as start level 28703 * if -1 : automatic start level selection, playback will start from level matching download bandwidth 28704 * (determined from download of first segment) 28705 */ 28706 set startLevel(newLevel) { 28707 logger.log(`set startLevel:${newLevel}`); 28708 // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel 28709 if (newLevel !== -1) { 28710 newLevel = Math.max(newLevel, this.minAutoLevel); 28711 } 28712 this.levelController.startLevel = newLevel; 28713 } 28714 28715 /** 28716 * Whether level capping is enabled. 28717 * Default value is set via `config.capLevelToPlayerSize`. 28718 */ 28719 get capLevelToPlayerSize() { 28720 return this.config.capLevelToPlayerSize; 28721 } 28722 28723 /** 28724 * Enables or disables level capping. If disabled after previously enabled, `nextLevelSwitch` will be immediately called. 28725 */ 28726 set capLevelToPlayerSize(shouldStartCapping) { 28727 const newCapLevelToPlayerSize = !!shouldStartCapping; 28728 if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) { 28729 if (newCapLevelToPlayerSize) { 28730 this.capLevelController.startCapping(); // If capping occurs, nextLevelSwitch will happen based on size. 28731 } else { 28732 this.capLevelController.stopCapping(); 28733 this.autoLevelCapping = -1; 28734 this.streamController.nextLevelSwitch(); // Now we're uncapped, get the next level asap. 28735 } 28736 this.config.capLevelToPlayerSize = newCapLevelToPlayerSize; 28737 } 28738 } 28739 28740 /** 28741 * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) 28742 */ 28743 get autoLevelCapping() { 28744 return this._autoLevelCapping; 28745 } 28746 28747 /** 28748 * Returns the current bandwidth estimate in bits per second, when available. Otherwise, `NaN` is returned. 28749 */ 28750 get bandwidthEstimate() { 28751 const { 28752 bwEstimator 28753 } = this.abrController; 28754 if (!bwEstimator) { 28755 return NaN; 28756 } 28757 return bwEstimator.getEstimate(); 28758 } 28759 set bandwidthEstimate(abrEwmaDefaultEstimate) { 28760 this.abrController.resetEstimator(abrEwmaDefaultEstimate); 28761 } 28762 28763 /** 28764 * get time to first byte estimate 28765 * @type {number} 28766 */ 28767 get ttfbEstimate() { 28768 const { 28769 bwEstimator 28770 } = this.abrController; 28771 if (!bwEstimator) { 28772 return NaN; 28773 } 28774 return bwEstimator.getEstimateTTFB(); 28775 } 28776 28777 /** 28778 * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) 28779 */ 28780 set autoLevelCapping(newLevel) { 28781 if (this._autoLevelCapping !== newLevel) { 28782 logger.log(`set autoLevelCapping:${newLevel}`); 28783 this._autoLevelCapping = newLevel; 28784 this.levelController.checkMaxAutoUpdated(); 28785 } 28786 } 28787 get maxHdcpLevel() { 28788 return this._maxHdcpLevel; 28789 } 28790 set maxHdcpLevel(value) { 28791 if (isHdcpLevel(value) && this._maxHdcpLevel !== value) { 28792 this._maxHdcpLevel = value; 28793 this.levelController.checkMaxAutoUpdated(); 28794 } 28795 } 28796 28797 /** 28798 * True when automatic level selection enabled 28799 */ 28800 get autoLevelEnabled() { 28801 return this.levelController.manualLevel === -1; 28802 } 28803 28804 /** 28805 * Level set manually (if any) 28806 */ 28807 get manualLevel() { 28808 return this.levelController.manualLevel; 28809 } 28810 28811 /** 28812 * min level selectable in auto mode according to config.minAutoBitrate 28813 */ 28814 get minAutoLevel() { 28815 const { 28816 levels, 28817 config: { 28818 minAutoBitrate 28819 } 28820 } = this; 28821 if (!levels) return 0; 28822 const len = levels.length; 28823 for (let i = 0; i < len; i++) { 28824 if (levels[i].maxBitrate >= minAutoBitrate) { 28825 return i; 28826 } 28827 } 28828 return 0; 28829 } 28830 28831 /** 28832 * max level selectable in auto mode according to autoLevelCapping 28833 */ 28834 get maxAutoLevel() { 28835 const { 28836 levels, 28837 autoLevelCapping, 28838 maxHdcpLevel 28839 } = this; 28840 let maxAutoLevel; 28841 if (autoLevelCapping === -1 && levels != null && levels.length) { 28842 maxAutoLevel = levels.length - 1; 28843 } else { 28844 maxAutoLevel = autoLevelCapping; 28845 } 28846 if (maxHdcpLevel) { 28847 for (let i = maxAutoLevel; i--;) { 28848 const hdcpLevel = levels[i].attrs['HDCP-LEVEL']; 28849 if (hdcpLevel && hdcpLevel <= maxHdcpLevel) { 28850 return i; 28851 } 28852 } 28853 } 28854 return maxAutoLevel; 28855 } 28856 get firstAutoLevel() { 28857 return this.abrController.firstAutoLevel; 28858 } 28859 28860 /** 28861 * next automatically selected quality level 28862 */ 28863 get nextAutoLevel() { 28864 return this.abrController.nextAutoLevel; 28865 } 28866 28867 /** 28868 * this setter is used to force next auto level. 28869 * this is useful to force a switch down in auto mode: 28870 * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example) 28871 * forced value is valid for one fragment. upon successful frag loading at forced level, 28872 * this value will be resetted to -1 by ABR controller. 28873 */ 28874 set nextAutoLevel(nextLevel) { 28875 this.abrController.nextAutoLevel = nextLevel; 28876 } 28877 28878 /** 28879 * get the datetime value relative to media.currentTime for the active level Program Date Time if present 28880 */ 28881 get playingDate() { 28882 return this.streamController.currentProgramDateTime; 28883 } 28884 get mainForwardBufferInfo() { 28885 return this.streamController.getMainFwdBufferInfo(); 28886 } 28887 28888 /** 28889 * Find and select the best matching audio track, making a level switch when a Group change is necessary. 28890 * Updates `hls.config.audioPreference`. Returns the selected track, or null when no matching track is found. 28891 */ 28892 setAudioOption(audioOption) { 28893 var _this$audioTrackContr; 28894 return (_this$audioTrackContr = this.audioTrackController) == null ? void 0 : _this$audioTrackContr.setAudioOption(audioOption); 28895 } 28896 /** 28897 * Find and select the best matching subtitle track, making a level switch when a Group change is necessary. 28898 * Updates `hls.config.subtitlePreference`. Returns the selected track, or null when no matching track is found. 28899 */ 28900 setSubtitleOption(subtitleOption) { 28901 var _this$subtitleTrackCo; 28902 (_this$subtitleTrackCo = this.subtitleTrackController) == null ? void 0 : _this$subtitleTrackCo.setSubtitleOption(subtitleOption); 28903 return null; 28904 } 28905 28906 /** 28907 * Get the complete list of audio tracks across all media groups 28908 */ 28909 get allAudioTracks() { 28910 const audioTrackController = this.audioTrackController; 28911 return audioTrackController ? audioTrackController.allAudioTracks : []; 28912 } 28913 28914 /** 28915 * Get the list of selectable audio tracks 28916 */ 28917 get audioTracks() { 28918 const audioTrackController = this.audioTrackController; 28919 return audioTrackController ? audioTrackController.audioTracks : []; 28920 } 28921 28922 /** 28923 * index of the selected audio track (index in audio track lists) 28924 */ 28925 get audioTrack() { 28926 const audioTrackController = this.audioTrackController; 28927 return audioTrackController ? audioTrackController.audioTrack : -1; 28928 } 28929 28930 /** 28931 * selects an audio track, based on its index in audio track lists 28932 */ 28933 set audioTrack(audioTrackId) { 28934 const audioTrackController = this.audioTrackController; 28935 if (audioTrackController) { 28936 audioTrackController.audioTrack = audioTrackId; 28937 } 28938 } 28939 28940 /** 28941 * get the complete list of subtitle tracks across all media groups 28942 */ 28943 get allSubtitleTracks() { 28944 const subtitleTrackController = this.subtitleTrackController; 28945 return subtitleTrackController ? subtitleTrackController.allSubtitleTracks : []; 28946 } 28947 28948 /** 28949 * get alternate subtitle tracks list from playlist 28950 */ 28951 get subtitleTracks() { 28952 const subtitleTrackController = this.subtitleTrackController; 28953 return subtitleTrackController ? subtitleTrackController.subtitleTracks : []; 28954 } 28955 28956 /** 28957 * index of the selected subtitle track (index in subtitle track lists) 28958 */ 28959 get subtitleTrack() { 28960 const subtitleTrackController = this.subtitleTrackController; 28961 return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1; 28962 } 28963 get media() { 28964 return this._media; 28965 } 28966 28967 /** 28968 * select an subtitle track, based on its index in subtitle track lists 28969 */ 28970 set subtitleTrack(subtitleTrackId) { 28971 const subtitleTrackController = this.subtitleTrackController; 28972 if (subtitleTrackController) { 28973 subtitleTrackController.subtitleTrack = subtitleTrackId; 28974 } 28975 } 28976 28977 /** 28978 * Whether subtitle display is enabled or not 28979 */ 28980 get subtitleDisplay() { 28981 const subtitleTrackController = this.subtitleTrackController; 28982 return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false;
28983 } 28984 28985 /** 28986 * Enable/disable subtitle display rendering 28987 */ 28988 set subtitleDisplay(value) { 28989 const subtitleTrackController = this.subtitleTrackController; 28990 if (subtitleTrackController) { 28991 subtitleTrackController.subtitleDisplay = value; 28992 } 28993 } 28994 28995 /** 28996 * get mode for Low-Latency HLS loading 28997 */ 28998 get lowLatencyMode() { 28999 return this.config.lowLatencyMode; 29000 } 29001 29002 /** 29003 * Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK. 29004 */ 29005 set lowLatencyMode(mode) { 29006 this.config.lowLatencyMode = mode; 29007 } 29008 29009 /** 29010 * Position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```) 29011 * @returns null prior to loading live Playlist 29012 */ 29013 get liveSyncPosition() { 29014 return this.latencyController.liveSyncPosition; 29015 } 29016 29017 /** 29018 * Estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) 29019 * @returns 0 before first playlist is loaded 29020 */ 29021 get latency() { 29022 return this.latencyController.latency; 29023 } 29024 29025 /** 29026 * maximum distance from the edge before the player seeks forward to ```hls.liveSyncPosition``` 29027 * configured using ```liveMaxLatencyDurationCount``` (multiple of target duration) or ```liveMaxLatencyDuration``` 29028 * @returns 0 before first playlist is loaded 29029 */ 29030 get maxLatency() { 29031 return this.latencyController.maxLatency; 29032 } 29033 29034 /** 29035 * target distance from the edge as calculated by the latency controller 29036 */ 29037 get targetLatency() { 29038 return this.latencyController.targetLatency; 29039 } 29040 29041 /** 29042 * the rate at which the edge of the current live playlist is advancing or 1 if there is none 29043 */ 29044 get drift() { 29045 return this.latencyController.drift; 29046 } 29047 29048 /** 29049 * set to true when startLoad is called before MANIFEST_PARSED event 29050 */ 29051 get forceStartLoad() { 29052 return this.streamController.forceStartLoad; 29053 } 29054} 29055Hls.defaultConfig = void 0; 29056 29057var root_5 = from_html(`<time id="trim-duration" class="svelte-1ffmt2w"> </time>`); 29058var root_4$1 = from_html(`<div class="component-wrapper svelte-1ffmt2w"><div class="waveform-container svelte-1ffmt2w"><div id="waveform" class="svelte-1ffmt2w"></div></div> <div class="timestamps svelte-1ffmt2w"><time id="time" class="svelte-1ffmt2w">0:00</time> <div><!> <time id="duration" class="svelte-1ffmt2w">0:00</time></div></div> <div class="subtitle-display svelte-1ffmt2w" data-testid="subtitle-display"></div> <!></div>`); 29059var root$1 = from_html(`<audio controls preload="metadata"></audio> <!>`, 1); 29060 29061function AudioPlayer($$anchor, $$props) { 29062 push$1($$props, true); 29063 29064 let value = prop($$props, 'value', 3, null), 29065 subtitles = prop($$props, 'subtitles', 3, null), 29066 dispatch_blob = prop($$props, 'dispatch_blob', 3, () => Promise.resolve()), 29067 interactive = prop($$props, 'interactive', 3, false), 29068 editable = prop($$props, 'editable', 3, true), 29069 trim_region_settings = prop($$props, 'trim_region_settings', 19, () => ({})), 29070 mode = prop($$props, 'mode', 15), 29071 handle_reset_value = prop($$props, 'handle_reset_value', 3, () => {}), 29072 playback_position = prop($$props, 'playback_position', 15); 29073 29074 let url = user_derived(() => value()?.url); 29075 let old_playback_position = state(0); 29076 let container; 29077 let waveform; 29078 let waveform_ready = state(false); 29079 let waveform_component_wrapper; 29080 let playing = state(false); 29081 let subtitle_container; 29082 let timeRef; 29083 let durationRef; 29084 let audio_duration = state(0); 29085 let trimDuration = state(0); 29086 let show_volume_slider = state(false); 29087 let audio_player; 29088 let stream_active = false; 29089 let subtitles_toggle = state(true); 29090 let subtitle_event_handlers = []; 29091 let use_waveform = user_derived(() => $$props.waveform_options.show_recording_waveform && !value()?.is_stream); 29092 29093 user_effect(() => { 29094 if (get(waveform_ready) && get(old_playback_position) !== playback_position() && get(audio_duration)) { 29095 waveform?.seekTo(playback_position() / get(audio_duration)); 29096 set(old_playback_position, playback_position(), true); 29097 } 29098 }); 29099 29100 const create_waveform = () => { 29101 waveform = u.create({ container, ...$$props.waveform_settings }); 29102 29103 if (subtitles() && waveform) { 29104 if (get(subtitles_toggle)) { 29105 add_subtitles_to_waveform(waveform, subtitles()); 29106 } else { 29107 hide_subtitles(); 29108 } 29109 } 29110 29111 waveform?.on("init", () => { 29112 set(waveform_ready, true); 29113 }); 29114
29115 waveform?.on("decode", (duration) => { 29116 set(audio_duration, duration, true); 29117 durationRef && (durationRef.textContent = format_time(duration)); 29118 }); 29119 29120 let firstTimeUpdate = true; 29121 29122 waveform?.on("timeupdate", (currentTime) => { 29123 timeRef && (timeRef.textContent = format_time(currentTime)); 29124 29125 if (firstTimeUpdate) { 29126 firstTimeUpdate = false; 29127 29128 return; 29129 } 29130 29131 playback_position(currentTime); 29132 set(old_playback_position, currentTime, true); 29133 }); 29134 29135 waveform?.on("interaction", () => { 29136 const currentTime = waveform?.getCurrentTime() || 0; 29137 29138 timeRef && (timeRef.textContent = format_time(currentTime)); 29139 playback_position(currentTime); 29140 set(old_playback_position, currentTime, true); 29141 }); 29142 29143 waveform?.on("ready", () => { 29144 if (!$$props.waveform_settings.autoplay) { 29145 waveform?.stop(); 29146 } else { 29147 waveform?.play(); 29148 } 29149 }); 29150 29151 waveform?.on("finish", () => { 29152 if ($$props.loop) { 29153 waveform?.play(); 29154 } else { 29155 set(playing, false); 29156 $$props.onstop?.(); 29157 } 29158 }); 29159 29160 waveform?.on("pause", () => { 29161 set(playing, false); 29162 $$props.onpause?.(); 29163 }); 29164 29165 waveform?.on("play", () => { 29166 set(playing, true); 29167 $$props.onplay?.(); 29168 }); 29169 29170 waveform?.on("load", () => { 29171 $$props.onload?.(); 29172 }); 29173 }; 29174 29175 user_effect(() => { 29176 if (get(url) && get(waveform_ready)) { 29177 untrack(() => { 29178 if (value()?.url && waveform) { 29179 waveform.load(value().url).catch((e) => { 29180 if (e.name !== "AbortError") { 29181 console.error("Waveform load error:", e); 29182 } 29183 }); 29184 } 29185 }); 29186 } 29187 }); 29188 29189 const handle_trim_audio = async (start, end) => { 29190 mode(""); 29191 29192 const decodedData = waveform?.getDecodedData(); 29193 29194 if (decodedData) { 29195 const trimmedBlob = await process_audio(decodedData, start, end, $$props.waveform_settings.sampleRate); 29196 29197 await dispatch_blob()([trimmedBlob], "change"); 29198 } 29199 29200 $$props.onedit?.(); 29201 }; 29202 29203 async function load_audio(data) { 29204 stream_active = false; 29205 29206 if ($$props.waveform_options.show_recording_waveform) { 29207 waveform?.load(data); 29208 } else if (audio_player) { 29209 audio_player.src = data; 29210 } 29211 } 29212 29213 user_effect(() => { 29214 if (subtitles() && waveform) { 29215 if (get(subtitles_toggle)) { 29216 add_subtitles_to_waveform(waveform, subtitles()); 29217 } else { 29218 hide_subtitles(); 29219 } 29220 } 29221 }); 29222 29223 function load_stream(value) { 29224 if (!value || !value.is_stream || !value.url) return; 29225 29226 if (Hls.isSupported() && !stream_active) { 29227 // Set config to start playback after 1 second of data received 29228 const hls = new Hls({ 29229 maxBufferLength: 1, 29230 maxMaxBufferLength: 1, 29231 lowLatencyMode: true 29232 }); 29233 29234 hls.loadSource(value.url); 29235 hls.attachMedia(audio_player); 29236 29237 hls.on(Hls.Events.MANIFEST_PARSED, function () { 29238 if ($$props.waveform_settings.autoplay) audio_player.play(); 29239 }); 29240 29241 hls.on(Hls.Events.ERROR, function (event, data) { 29242 console.error("HLS error:", event, data); 29243 29244 if (data.fatal) { 29245 switch (data.type) { 29246 case Hls.ErrorTypes.NETWORK_ERROR: 29247 console.error("Fatal network error encountered, trying to recover"); 29248 hls.startLoad(); 29249 break; 29250 29251 case Hls.ErrorTypes.MEDIA_ERROR: 29252 console.error("Fatal media error encountered, trying to recover"); 29253 hls.recoverMediaError(); 29254 break; 29255 29256 default: 29257 console.error("Fatal error, cannot recover"); 29258 hls.destroy(); 29259 break; 29260 } 29261 } 29262 }); 29263 29264 stream_active = true; 29265 } else if (!stream_active) { 29266 audio_player.src = value.url; 29267 29268 if ($$props.waveform_settings.autoplay) audio_player.play(); 29269 29270 stream_active = true; 29271 } 29272 } 29273 29274 user_effect(() => { 29275 if (audio_player && get(url) && get(waveform_ready) && get(url)) { 29276 load_audio(get(url)); 29277 } 29278 }); 29279 29280 user_effect(() => { 29281 if (audio_player && value()?.is_stream) { 29282 load_stream(value()); 29283 } 29284 }); 29285 29286 onMount(() => { 29287 create_waveform(); 29288 29289 const handleKeydown = (e) => { 29290 if (!waveform || get(show_volume_slider)) return; 29291 29292 const is_focused_in_waveform = waveform_component_wrapper && waveform_component_wrapper.contains(document.activeElement); 29293 29294 if (!is_focused_in_waveform) return; 29295 29296 if (e.key === "ArrowRight" && mode() !== "edit") { 29297 skip_audio(waveform, 0.1); 29298 } else if (e.key === "ArrowLeft" && mode() !== "edit") { 29299 skip_audio(waveform, -0.1); 29300 } 29301 }; 29302 29303 window.addEventListener("keydown", handleKeydown); 29304 29305 return () => { 29306 waveform?.destroy(); 29307 window.removeEventListener("keydown", handleKeydown); 29308 }; 29309 }); 29310 29311 async function add_subtitles_to_waveform(wavesurfer, subtitle_data) { 29312 clear_subtitles(); 29313 29314 try { 29315 let subtitles; 29316 29317 if (Array.isArray(subtitle_data)) { 29318 subtitles = subtitle_data; 29319 } else { 29320 const response = await fetch(subtitle_data); 29321 const subtitle_content = await response.text(); 29322 29323 subtitles = parse_subtitles(subtitle_content); 29324 } 29325 29326 if (subtitles.length > 0) { 29327 let current_subtitle = ""; 29328 29329 if (subtitle_container) { 29330 subtitle_container.style.display = ""; 29331 29332 const audioProcessHandler = (time) => { 29333 const subtitle = subtitles.find((s) => time >= s.start && time <= s.end); 29334 29335 if (subtitle && subtitle.text !== current_subtitle) { 29336 current_subtitle = subtitle.text; 29337 subtitle_container.textContent = current_subtitle; 29338 } else if (!subtitle && current_subtitle !== "") { 29339 current_subtitle = ""; 29340 subtitle_container.textContent = ""; 29341 } 29342 }; 29343 29344 wavesurfer.on("audioprocess", audioProcessHandler); 29345 29346 subtitle_event_handlers.push(() => { 29347 wavesurfer.un("audioprocess", audioProcessHandler); 29348 }); 29349 } 29350 } 29351 } catch(error) {} 29352 } 29353 29354 function hide_subtitles() { 29355 if (subtitle_container) { 29356 subtitle_container.style.display = "none"; 29357 } 29358 } 29359 29360 function clear_subtitles() { 29361 if (subtitle_container) { 29362 subtitle_container.textContent = ""; 29363 } 29364
29365 subtitle_event_handlers.forEach((handler) => handler()); 29366 subtitle_event_handlers = []; 29367 } 29368 29369 function parse_subtitles(subtitle_content) { 29370 const lines = subtitle_content.split("\n"); 29371 const subtitles = []; 29372 29373 for (let i = 0; i < lines.length; i++) { 29374 const line = lines[i].trim(); 29375 29376 if (line.includes(" --> ")) { 29377 const [start_time, end_time] = line.split(" --> "); 29378 const start = parse_time_to_seconds(start_time); 29379 const end = parse_time_to_seconds(end_time); 29380 let text = ""; 29381 29382 for (let j = i + 1; j < lines.length && lines[j].trim() !== ""; j++) { 29383 if (text) text += " "; 29384 29385 text += lines[j].trim(); 29386 } 29387 29388 if (text) { 29389 subtitles.push({ start, end, text }); 29390 } 29391 } 29392 } 29393 29394 return subtitles; 29395 } 29396 29397 function parse_time_to_seconds(time_str) { 29398 const parts = time_str.split(":"); 29399 29400 if (parts.length === 3) { 29401 const hours = parseInt(parts[0]); 29402 const minutes = parseInt(parts[1]); 29403 const seconds = parseFloat(parts[2]); 29404 29405 return hours * 3600 + minutes * 60 + seconds; 29406 } 29407 29408 return 0; 29409 } 29410 29411 var fragment = root$1(); 29412 var audio = first_child(fragment); 29413 let classes; 29414 29415 bind_this(audio, ($$value) => audio_player = $$value, () => audio_player); 29416 29417 var node = sibling(audio, 2); 29418 29419 { 29420 var consequent = ($$anchor) => { 29421 Empty($$anchor, { 29422 size: 'small', 29423 children: ($$anchor, $$slotProps) => { 29424 Music($$anchor); 29425 }, 29426 $$slots: { default: true } 29427 }); 29428 }; 29429 29430 var alternate = ($$anchor) => { 29431 var fragment_3 = comment(); 29432 var node_1 = first_child(fragment_3); 29433 29434 { 29435 var consequent_2 = ($$anchor) => { 29436 var div = root_4$1(); 29437 var div_1 = child(div); 29438 var div_2 = child(div_1); 29439 let styles; 29440 29441 bind_this(div_2, ($$value) => container = $$value, () => container); 29442 reset(div_1); 29443 29444 var div_3 = sibling(div_1, 2); 29445 var time_1 = child(div_3); 29446 29447 bind_this(time_1, ($$value) => timeRef = $$value, () => timeRef); 29448 29449 var div_4 = sibling(time_1, 2); 29450 var node_2 = child(div_4); 29451 29452 { 29453 var consequent_1 = ($$anchor) => { 29454 var time_2 = root_5(); 29455 var text_1 = child(time_2, true); 29456 29457 reset(time_2); 29458 template_effect(($0) => set_text(text_1, $0), [() => format_time(get(trimDuration))]); 29459 append($$anchor, time_2); 29460 }; 29461 29462 if_block(node_2, ($$render) => { 29463 if (mode() === "edit" && get(trimDuration) > 0) $$render(consequent_1); 29464 }); 29465 } 29466 29467 var time_3 = sibling(node_2, 2); 29468 29469 bind_this(time_3, ($$value) => durationRef = $$value, () => durationRef); 29470 reset(div_4); 29471 reset(div_3); 29472 29473 var div_5 = sibling(div_3, 2); 29474 29475 bind_this(div_5, ($$value) => subtitle_container = $$value, () => subtitle_container); 29476 29477 var node_3 = sibling(div_5, 2); 29478 29479 { 29480 let $0 = user_derived(() => subtitles() !== null); 29481 29482 WaveformControls(node_3, { 29483 get container() { 29484 return container; 29485 }, 29486 29487 get waveform() { 29488 return waveform; 29489 }, 29490 29491 get playing() { 29492 return get(playing); 29493 }, 29494 29495 get audio_duration() { 29496 return get(audio_duration); 29497 }, 29498 29499 get i18n() { 29500 return $$props.i18n; 29501 }, 29502 29503 get interactive() { 29504 return interactive(); 29505 }, 29506 handle_trim_audio, 29507 get show_redo() { 29508 return interactive(); 29509 }, 29510 29511 get handle_reset_value() { 29512 return handle_reset_value(); 29513 }, 29514 29515 get waveform_options() { 29516 return $$props.waveform_options; 29517 }, 29518 29519 get trim_region_settings() { 29520 return trim_region_settings(); 29521 }, 29522 29523 get editable() { 29524 return editable(); 29525 }, 29526 29527 get show_subtitles() { 29528 return get($0); 29529 }, 29530 29531 get mode() { 29532 return mode(); 29533 }, 29534 29535 set mode($$value) { 29536 mode($$value); 29537 }, 29538 29539 get trimDuration() { 29540 return get(trimDuration); 29541 }, 29542 29543 set trimDuration($$value) { 29544 set(trimDuration, $$value, true); 29545 }, 29546 29547 get show_volume_slider() { 29548 return get(show_volume_slider); 29549 }, 29550 29551 set show_volume_slider($$value) { 29552 set(show_volume_slider, $$value, true); 29553 }, 29554
29555 get subtitles_toggle() { 29556 return get(subtitles_toggle); 29557 }, 29558 29559 set subtitles_toggle($$value) { 29560 set(subtitles_toggle, $$value, true); 29561 } 29562 }); 29563 } 29564 29565 reset(div); 29566 bind_this(div, ($$value) => waveform_component_wrapper = $$value, () => waveform_component_wrapper); 29567 29568 template_effect(() => { 29569 set_attribute(div, 'data-testid', $$props.label ? "waveform-" + $$props.label : "unlabelled-audio"); 29570 styles = set_style(div_2, '', styles, { height: container ? null : "58px" }); 29571 }); 29572 29573 append($$anchor, div); 29574 }; 29575 29576 if_block( 29577 node_1, 29578 ($$render) => { 29579 if (get(use_waveform)) $$render(consequent_2); 29580 }, 29581 true 29582 ); 29583 } 29584 29585 append($$anchor, fragment_3); 29586 }; 29587 29588 if_block(node, ($$render) => { 29589 if (value() === null) $$render(consequent); else $$render(alternate, false); 29590 }); 29591 } 29592 29593 template_effect(() => { 29594 classes = set_class(audio, 1, 'standard-player svelte-1ffmt2w', null, classes, { hidden: get(use_waveform) }); 29595 audio.autoplay = $$props.waveform_settings.autoplay; 29596 }); 29597 29598 event('load', audio, function (...$$args) { 29599 $$props.onload?.apply(this, $$args); 29600 }); 29601 29602 event('ended', audio, () => $$props.onstop?.()); 29603 event('play', audio, () => $$props.onplay?.()); 29604 append($$anchor, fragment); 29605 pop(); 29606} 29607 29608var root_4 = from_html(`<!> <!>`, 1); 29609var root_3 = from_html(`<!> <!>`, 1); 29610var root = from_html(`<!> <!>`, 1); 29611 29612function StaticAudio($$anchor, $$props) { 29613 push$1($$props, true); 29614 29615 let value = prop($$props, 'value', 3, null), 29616 subtitles = prop($$props, 'subtitles', 3, null), 29617 show_label = prop($$props, 'show_label', 3, true), 29618 buttons = prop($$props, 'buttons', 19, () => []), 29619 on_custom_button_click = prop($$props, 'on_custom_button_click', 3, null), 29620 waveform_settings = prop($$props, 'waveform_settings', 19, () => ({})), 29621 waveform_options = prop($$props, 'waveform_options', 19, () => ({ show_recording_waveform: true })), 29622 editable = prop($$props, 'editable', 3, true), 29623 display_icon_button_wrapper_top_corner = prop($$props, 'display_icon_button_wrapper_top_corner', 3, false), 29624 minimal = prop($$props, 'minimal', 3, false), 29625 playback_position = prop($$props, 'playback_position', 15); 29626 29627 user_effect(() => { 29628 if (value()) { 29629 $$props.onchange?.(value()); 29630 } 29631 }); 29632 29633 var fragment = root(); 29634 var node = first_child(fragment); 29635 29636 { 29637 let $0 = user_derived(() => $$props.label || $$props.i18n("audio.audio")); 29638 29639 BlockLabel(node, { 29640 get show_label() { 29641 return show_label(); 29642 }, 29643 29644 get Icon() { 29645 return Music; 29646 }, 29647 float: false, 29648 get label() { 29649 return get($0); 29650 } 29651 }); 29652 } 29653 29654 var node_1 = sibling(node, 2); 29655 29656 { 29657 var consequent_3 = ($$anchor) => { 29658 var fragment_1 = comment(); 29659 var node_2 = first_child(fragment_1); 29660 29661 { 29662 var consequent = ($$anchor) => { 29663 { 29664 let $0 = user_derived(() => $$props.label || $$props.i18n("audio.audio")); 29665 29666 MinimalAudioPlayer($$anchor, { 29667 get value() { 29668 return value(); 29669 }, 29670 29671 get label() { 29672 return get($0); 29673 }, 29674 29675 get loop() { 29676 return $$props.loop; 29677 } 29678 }); 29679 } 29680 }; 29681 29682 var alternate = ($$anchor) => { 29683 var fragment_3 = root_3(); 29684 var node_3 = first_child(fragment_3); 29685 29686 IconButtonWrapper(node_3, { 29687 get display_top_corner() { 29688 return display_icon_button_wrapper_top_corner(); 29689 }, 29690 29691 get buttons() { 29692 return buttons(); 29693 }, 29694 29695 get on_custom_button_click() { 29696 return on_custom_button_click(); 29697 }, 29698 29699 children: ($$anchor, $$slotProps) => { 29700 var fragment_4 = root_4(); 29701 var node_4 = first_child(fragment_4); 29702 29703 { 29704 var consequent_1 = ($$anchor) => { 29705 { 29706 let $0 = user_derived(() => value().is_stream 29707 ? value().url?.replace("playlist.m3u8", "playlist-file") 29708 : value().url); 29709 29710 let $1 = user_derived(() => value().orig_name || value().path); 29711 29712 DownloadLink($$anchor, { 29713 get href() { 29714 return get($0); 29715 }, 29716 29717 get download() { 29718 return get($1); 29719 }, 29720 29721 children: ($$anchor, $$slotProps) => { 29722 { 29723 let $0 = user_derived(() => $$props.i18n("common.download")); 29724 29725 IconButton($$anchor, { 29726 get Icon() { 29727 return Download; 29728 }, 29729 29730 get label() { 29731 return get($0); 29732 } 29733 }); 29734 } 29735 }, 29736 $$slots: { default: true } 29737 }); 29738 } 29739 }; 29740 29741 if_block(node_4, ($$render) => { 29742 if (buttons().some((btn) => typeof btn === "string" && btn === "download")) $$render(consequent_1); 29743 }); 29744 } 29745 29746 var node_5 = sibling(node_4, 2); 29747 29748 { 29749 var consequent_2 = ($$anchor) => { 29750 ShareButton($$anchor, { 29751 get i18n() { 29752 return $$props.i18n; 29753 }, 29754
29755 get onerror() { 29756 return $$props.onerror; 29757 }, 29758 29759 get onshare() { 29760 return $$props.onshare; 29761 }, 29762 29763 formatter: async (fileData) => { 29764 if (!fileData || !fileData.url) return ""; 29765 29766 let url = await uploadToHuggingFace(fileData.url); 29767 29768 return `<audio controls src="${url}"></audio>`; 29769 }, 29770 29771 get value() { 29772 return value(); 29773 } 29774 }); 29775 }; 29776 29777 if_block(node_5, ($$render) => { 29778 if (buttons().some((btn) => typeof btn === "string" && btn === "share")) $$render(consequent_2); 29779 }); 29780 } 29781 29782 append($$anchor, fragment_4); 29783 }, 29784 $$slots: { default: true } 29785 }); 29786 29787 var node_6 = sibling(node_3, 2); 29788 29789 { 29790 let $0 = user_derived(() => Array.isArray(subtitles()) ? subtitles() : subtitles()?.url); 29791 29792 AudioPlayer(node_6, { 29793 get value() { 29794 return value(); 29795 }, 29796 29797 get subtitles() { 29798 return get($0); 29799 }, 29800 29801 get label() { 29802 return $$props.label; 29803 }, 29804 29805 get i18n() { 29806 return $$props.i18n; 29807 }, 29808 29809 get waveform_settings() { 29810 return waveform_settings(); 29811 }, 29812 29813 get waveform_options() { 29814 return waveform_options(); 29815 }, 29816 29817 get editable() { 29818 return editable(); 29819 }, 29820 29821 get loop() { 29822 return $$props.loop; 29823 }, 29824 29825 get onpause() { 29826 return $$props.onpause; 29827 }, 29828 29829 get onplay() { 29830 return $$props.onplay; 29831 }, 29832 29833 get onstop() { 29834 return $$props.onstop; 29835 }, 29836 onload: () => {}, 29837 get playback_position() { 29838 return playback_position(); 29839 }, 29840 29841 set playback_position($$value) { 29842 playback_position($$value); 29843 } 29844 }); 29845 } 29846 29847 append($$anchor, fragment_3); 29848 }; 29849 29850 if_block(node_2, ($$render) => { 29851 if (minimal()) $$render(consequent); else $$render(alternate, false); 29852 }); 29853 } 29854 29855 append($$anchor, fragment_1); 29856 }; 29857 29858 var alternate_1 = ($$anchor) => { 29859 Empty($$anchor, { 29860 size: 'small', 29861 children: ($$anchor, $$slotProps) => { 29862 Music($$anchor); 29863 }, 29864 $$slots: { default: true } 29865 }); 29866 }; 29867 29868 if_block(node_1, ($$render) => { 29869 if (value() !== null) $$render(consequent_3); else $$render(alternate_1, false); 29870 }); 29871 } 29872 29873 append($$anchor, fragment); 29874 pop(); 29875} 29876 29877const StaticAudio$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ 29878 __proto__: null, 29879 default: StaticAudio 29880}, Symbol.toStringTag, { value: 'Module' })); 29881 29882export { AudioPlayer as A, StaticAudio as S, WaveformControls as W, StaticAudio$1 as a }; 29883//# sourceMappingURL=StaticAudio-CWETG3mI.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.