@videojs/spf 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +51 -0
  2. package/dist/index.js +1 -0
  3. package/package.json +63 -0
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @videojs/spf
2
+
3
+ Stream Processing Framework for Video.js 10
4
+
5
+ ## Overview
6
+
7
+ SPF is a lightweight, bundle-size-optimized HLS playback engine for Video.js 10.
8
+
9
+ **Target:** < 20KB (minified + gzipped)
10
+ **Target Date:** February 27, 2026
11
+ **Status:** Active development
12
+
13
+ ## V1 Feature Set
14
+
15
+ - CMAF HLS VOD playback
16
+ - Basic ABR (EWMA throughput)
17
+ - WebVTT captions
18
+ - MSE + MMS support
19
+ - Chrome, Safari, Firefox, Edge (latest)
20
+
21
+ ## Development
22
+
23
+ ```bash
24
+ # Install dependencies
25
+ pnpm install
26
+
27
+ # Run tests
28
+ pnpm test
29
+
30
+ # Build
31
+ pnpm build
32
+
33
+ # Typecheck
34
+ pnpm typecheck
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ See `.claude/spf-breakdown/` for:
40
+ - Complete work breakdown (58 issues)
41
+ - Architecture decisions
42
+ - Implementation timeline
43
+
44
+ ## Reference
45
+
46
+ Spike code available in `.archive/spf-xstate-poc/` for pattern reference.
47
+
48
+ ## GitHub Issues
49
+
50
+ - Epics: #384, #385, #386, #387
51
+ - All issues: https://github.com/videojs/v10/issues?q=is:issue+label:spf
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{isNil as e,isUndefined as t}from"@videojs/utils/predicate";import{listen as n}from"@videojs/utils/dom";const r=Symbol(`@videojs/event-stream`);function i(){let e=new Set;return{[r]:!0,dispatch(t){let n=Array.from(e);for(let e of n)e(t)},subscribe(t){return e.add(t),()=>e.delete(t)}}}function a(e){let t=t=>{let n=Array(e.length),r=Array(e.length).fill(!1),i=[];for(let a=0;a<e.length;a++){let o=e[a].subscribe(e=>{n[a]=e,r[a]=!0,r.every(e=>e)&&t([...n])});i.push(o)}return()=>{for(let e of i)e()}};return{subscribe(e,n,r){if(n===void 0)return t(e);let i=e,a=n,o=r?.equalityFn??Object.is,s,c=!1;return t(e=>{let t=i(e);(!c||!o(s,t))&&(s=t,c=!0,a(t))})}}}function o(e){return`segments`in e}function s(e){return e.duration!==void 0}const c={video:`selectedVideoTrackId`,audio:`selectedAudioTrackId`,text:`selectedTextTrackId`},l={video:`videoBuffer`,audio:`audioBuffer`};function u(e,t){let{presentation:n}=e;if(!n||!(`id`in n))return;let r=e[c[t]];return n.selectionSets.find(({type:e})=>e===t)?.switchingSets[0]?.tracks.find(({id:e})=>e===r)}function d(e){return e.presentation?!!(e.selectedVideoTrackId||e.selectedAudioTrackId):!1}function f(e){if(!d(e))return!1;let{presentation:t}=e;if(t.duration!==void 0)return!1;let n=e.selectedVideoTrackId?u(e,`video`):void 0,r=e.selectedAudioTrackId?u(e,`audio`):void 0;return!!(n&&o(n)||r&&o(r))}function p(e){let t=e.selectedVideoTrackId?u(e,`video`):void 0;if(t&&o(t))return t.duration;let n=e.selectedAudioTrackId?u(e,`audio`):void 0;if(n&&o(n))return n.duration}function m({state:e}){return a([e]).subscribe(([t])=>{if(!f(t))return;let n=p(t);if(n===void 0||!Number.isFinite(n))return;let{presentation:r}=t;e.patch({presentation:{...r,duration:n}})})}function h(e){return Math.exp(Math.log(.5)/e)}function g(e,t,n,r){let i=h(r)**n;return t*(1-i)+i*e}function _(e,t,n){return t===0?0:e/(1-h(n)**t)}const v={fastHalfLife:2,slowHalfLife:5,minTotalBytes:128e3,minBytes:16e3,minDuration:5};function ee(e,t,n,r=v){let i=e.bytesSampled+n;if(n<r.minBytes||t<r.minDuration)return{...e,bytesSampled:i};let a=8e3*n/t,o=t/1e3;return{fastEstimate:g(e.fastEstimate,a,o,r.fastHalfLife),fastTotalWeight:e.fastTotalWeight+o,slowEstimate:g(e.slowEstimate,a,o,r.slowHalfLife),slowTotalWeight:e.slowTotalWeight+o,bytesSampled:i}}function te(e,t,n=v){if(e.bytesSampled<n.minTotalBytes)return t;let r=_(e.fastEstimate,e.fastTotalWeight,n.fastHalfLife),i=_(e.slowEstimate,e.slowTotalWeight,n.slowHalfLife);return Math.min(r,i)}const ne={safetyMargin:.85};function y(e,t,n=ne){if(e.length===0)return;let r=e.slice().sort((e,t)=>e.bandwidth-t.bandwidth),i;for(let e of r)t>=e.bandwidth/n.safetyMargin&&(!i||e.bandwidth>i.bandwidth||e.bandwidth===i.bandwidth&&re(e,i))&&(i=e);return i??r[0]}function re(e,t){return(e.width??0)*(e.height??0)>(t.width??0)*(t.height??0)}const b={safetyMargin:.85,minUpgradeInterval:8e3,defaultBandwidth:5e6};function ie(e){return(e.selectionSets?.find(e=>e.type===`video`))?.switchingSets[0]?.tracks??[]}function ae({state:e},t={}){let n=t.safetyMargin??b.safetyMargin,r=t.minUpgradeInterval??b.minUpgradeInterval,i=t.defaultBandwidth??b.defaultBandwidth,a=Date.now(),o=!0;return e.subscribe(t=>{let{presentation:s,bandwidthState:c,selectedVideoTrackId:l,abrDisabled:u}=t;if(u===!0||!s||!c)return;let d=ie(s);if(d.length===0)return;let f=o;o=!1;let p=y(d,te(c,i),{safetyMargin:n});if(!p||p.id===l)return;let m=d.find(e=>e.id===l);if(!m||p.bandwidth>m.bandwidth){let e=Date.now();if(!f&&e-a<r)return;a=e}e.patch({selectedVideoTrackId:p.id})})}async function x(e,t){let n=new Headers(t?.headers);if(e.byteRange){let{start:t,end:r}=e.byteRange;n.set(`Range`,`bytes=${t}-${r}`)}let r=new Request(e.url,{method:`GET`,headers:n,...t});return fetch(r)}async function S(e,t){return(await x(e,t)).arrayBuffer()}function C(e){return e.text()}function w(){return`${Date.now()}-${Math.floor(Math.random()*1e6)}`}function oe(e){let t=new Map;for(let n of e.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)){let e=n[1],r=n[2]??n[3]??``;e&&t.set(e,r)}return t}function se(e){let t=/^(\d+)x(\d+)$/.exec(e);return t?{width:Number.parseInt(t[1],10),height:Number.parseInt(t[2],10)}:null}function ce(e){let t=Number.parseFloat(e);if(!(Number.isNaN(t)||t<=0))return Math.abs(t-23.976)<.01?{frameRateNumerator:24e3,frameRateDenominator:1001}:Math.abs(t-29.97)<.01?{frameRateNumerator:3e4,frameRateDenominator:1001}:Math.abs(t-59.94)<.01?{frameRateNumerator:6e4,frameRateDenominator:1001}:(t%1,{frameRateNumerator:Math.round(t)})}function T(e){let t=e.split(`,`).map(e=>e.trim()),n={};for(let e of t)e.startsWith(`avc1.`)||e.startsWith(`hvc1.`)||e.startsWith(`hev1.`)?n.video=e:e.startsWith(`mp4a.`)&&(n.audio=e);return n}function le(e){let t=e.split(`,`)[0]??e,n=Number.parseFloat(t);return Number.isNaN(n)?0:n}function E(e,t){let n=/^(\d+)(?:@(\d+))?$/.exec(e);if(!n)return null;let r=Number.parseInt(n[1],10);if(Number.isNaN(r))return null;let i;if(n[2]!==void 0){if(i=Number.parseInt(n[2],10),Number.isNaN(i))return null}else if(t!==void 0)i=t;else return null;return{start:i,end:i+r-1}}function ue(e){let t=oe(e);return{get(e){return t.get(e)},getInt(e,n){let r=t.get(e);if(r===void 0)return n;let i=Number.parseInt(r,10);return Number.isNaN(i)?n:i},getFloat(e,n){let r=t.get(e);if(r===void 0)return n;let i=Number.parseFloat(r);return Number.isNaN(i)?n:i},getBool(e){return t.get(e)===`YES`},getResolution(e){let n=t.get(e);if(n)return se(n)??void 0},getFrameRate(e){let n=t.get(e);if(n)return ce(n)}}}function D(e,t){let n=`#${t}:`;return e.startsWith(n)?ue(e.slice(n.length)):null}function O(e,t){return new URL(e,t).href}function de(e,t){let n=t.url,r=e.split(/\r?\n/),i=[],a=[],o=[],s=null;for(let e of r){let t=e.trim();if(!t||t.startsWith(`#`)&&!t.startsWith(`#EXT`)||t===`#EXTM3U`||t.startsWith(`#EXT-X-VERSION:`)||t.startsWith(`#EXT-X-INDEPENDENT-SEGMENTS`))continue;let r=D(t,`EXT-X-MEDIA`);if(r){let e=r.get(`TYPE`),t=r.get(`GROUP-ID`),i=r.get(`NAME`);if(e===`AUDIO`&&t&&i){let e=r.get(`URI`);a.push({groupId:t,name:i,language:r.get(`LANGUAGE`),uri:e?O(e,n):void 0,default:r.getBool(`DEFAULT`),autoselect:r.getBool(`AUTOSELECT`)})}if(e===`SUBTITLES`&&t&&i){let e=r.get(`URI`);e&&o.push({groupId:t,name:i,language:r.get(`LANGUAGE`),uri:O(e,n),default:r.getBool(`DEFAULT`),autoselect:r.getBool(`AUTOSELECT`),forced:r.getBool(`FORCED`)})}continue}let c=D(t,`EXT-X-STREAM-INF`);if(c){s={bandwidth:c.getInt(`BANDWIDTH`,0),resolution:c.getResolution(`RESOLUTION`),codecs:c.get(`CODECS`),frameRate:c.getFrameRate(`FRAME-RATE`),audioGroupId:c.get(`AUDIO`)};continue}!t.startsWith(`#`)&&s&&(i.push({...s,uri:O(t,n)}),s=null)}let c=[],l=[];for(let e of i){if(!e.codecs){c.push(e);continue}let t=T(e.codecs);e.codecs.split(`,`).length===1&&t.audio&&!t.video?l.push(e):c.push(e)}let u=c.map(e=>{let t=e.codecs?T(e.codecs):void 0,n={type:`video`,id:w(),url:e.uri,bandwidth:e.bandwidth,mimeType:`video/mp4`,codecs:[]};return e.resolution?.width!==void 0&&(n.width=e.resolution.width),e.resolution?.height!==void 0&&(n.height=e.resolution.height),t?.video&&(n.codecs=[t.video]),e.frameRate&&(n.frameRate=e.frameRate),e.audioGroupId&&(n.audioGroupId=e.audioGroupId),n}),d=l.map(e=>{let t=e.codecs?T(e.codecs):void 0;return{type:`audio`,id:w(),url:e.uri,bandwidth:e.bandwidth,mimeType:`audio/mp4`,codecs:t?.audio?[t.audio]:[],groupId:e.audioGroupId||`default`,name:`Default`,sampleRate:48e3,channels:2}}),f=[...a.map(e=>{let t;for(let n of i)if(n.audioGroupId===e.groupId&&n.codecs){let e=T(n.codecs);if(e.audio){t=[e.audio];break}}let n={type:`audio`,id:w(),url:e.uri??``,groupId:e.groupId,name:e.name,mimeType:`audio/mp4`,bandwidth:0,sampleRate:48e3,channels:2,codecs:[]};return e.language&&(n.language=e.language),t&&(n.codecs=t),e.default&&(n.default=e.default),e.autoselect&&(n.autoselect=e.autoselect),n}),...d],p=o.map(e=>{let t={type:`text`,id:w(),url:e.uri,groupId:e.groupId,label:e.name,kind:`subtitles`,mimeType:`text/vtt`,bandwidth:0};return e.language&&(t.language=e.language),e.default&&e.autoselect&&(t.default=!0),e.autoselect&&(t.autoselect=e.autoselect),e.forced&&(t.forced=e.forced),t}),m=[];if(u.length>0){let e={id:w(),type:`video`,tracks:u},t={id:w(),type:`video`,switchingSets:[e]};m.push(t)}if(f.length>0){let e={id:w(),type:`audio`,tracks:f},t={id:w(),type:`audio`,switchingSets:[e]};m.push(t)}if(p.length>0){let e={id:w(),type:`text`,tracks:p},t={id:w(),type:`text`,switchingSets:[e]};m.push(t)}return{id:w(),url:t.url,startTime:0,selectionSets:m}}function fe(e){return e!==void 0&&`url`in e&&!(`id`in e)}function pe(e){return fe(e.presentation)}function me(e,t){let{preload:n}=e;return[`auto`,`metadata`].includes(n)||t.type===`play`}function he(e,t){return t.subscribe(t=>{let n=t.mediaElement?.preload||void 0;e.patch({preload:n})})}function ge({state:e,events:t}){let n=!1,r=null,i=a([e,t]).subscribe(async([t,i])=>{if(!(!pe(t)||!me(t,i)||n))try{n=!0,r=new AbortController;let{presentation:i}=t,a=de(await C(await x(i,{signal:r.signal})),i);e.patch({presentation:a})}catch(e){if(e instanceof Error&&e.name===`AbortError`)return;throw e}finally{n=!1,r=null}});return()=>{r?.abort(),i()}}function _e(e,t){let n=e.split(/\r?\n/),r=t.url,i=[],a,o,s=0,c,l=0,u=0,d;for(let e of n){let t=e.trim();if(!t||t.startsWith(`#`)&&!t.startsWith(`#EXT`)||t===`#EXTM3U`||t.startsWith(`#EXT-X-VERSION:`)||t.startsWith(`#EXT-X-TARGETDURATION:`)||t.startsWith(`#EXT-X-PLAYLIST-TYPE:`)||t.startsWith(`#EXT-X-INDEPENDENT-SEGMENTS`))continue;let n=D(t,`EXT-X-MAP`);if(n){let e=n.get(`URI`);if(e){a=O(e,r);let t=n.get(`BYTERANGE`);t&&(o=E(t,0)??void 0)}continue}if(t.startsWith(`#EXTINF:`)){s=le(t.slice(8));continue}if(t.startsWith(`#EXT-X-BYTERANGE:`)){c=E(t.slice(17),d)??void 0;continue}if(t!==`#EXT-X-ENDLIST`&&!t.startsWith(`#`)&&s>0){let e={id:`segment-${u}`,url:O(t,r),duration:s,startTime:l};c?(e.byteRange=c,d=c.end+1):d=void 0,i.push(e),l+=s,u++,s=0,c=void 0}}let f=l,p=t.type===`text`&&!a?void 0:a?{url:a,...o?{byteRange:o}:{}}:{url:``};return{...t,startTime:0,duration:f,segments:i,initialization:p}}var k=class{id;#e;#t=new AbortController;#n;#r=`pending`;#i=void 0;#a=void 0;constructor(e,t){this.#e=e;let n=t?.id;this.id=typeof n==`function`?n():n??w(),this.#n=t?.signal?AbortSignal.any([this.#t.signal,t.signal]):this.#t.signal}get status(){return this.#r}get value(){return this.#i}get error(){return this.#a}async run(){this.#r=`running`;try{let e=await this.#e(this.#n);return this.#i=e,this.#r=`done`,e}catch(e){throw this.#a=e,this.#r=`error`,e}}abort(){this.#t.abort()}},ve=class{#e=new Map;schedule(e){this.#e.has(e.id)||(this.#e.set(e.id,e),e.run().catch(e=>{if(!(e instanceof Error&&e.name===`AbortError`))throw e}).finally(()=>{this.#e.delete(e.id)}))}abortAll(){for(let e of this.#e.values())e.abort();this.#e.clear()}},ye=class{#e=Promise.resolve();#t=new Set;#n=null;schedule(e){let t=e;this.#t.add(t);let n=this.#e.then(()=>(this.#t.delete(t),this.#n=t,e.run())).finally(()=>{this.#n=null});return this.#e=n.then(()=>{},()=>{}),n}abortAll(){for(let e of this.#t)e.abort();this.#t.clear(),this.#n?.abort()}destroy(){this.abortAll()}};function be(e,t){let n=u(e,t.type);return n?!o(n):!1}function xe(e,t){return!0}function A(e,t){let n=t.id;return{...e,selectionSets:e.selectionSets.map(e=>({...e,switchingSets:e.switchingSets.map(e=>({...e,tracks:e.tracks.map(e=>e.id===n?t:e)}))}))}}function j({state:e,events:t},n){let r=new ve,i=a([e,t]).subscribe(([t,i])=>{if(!be(t,n)||!xe(t,i))return;let a=u(t,n.type);if(!a)return;let o=a;r.schedule(new k(async t=>{let n=_e(await C(await x(o,{signal:t})),o),r=e.current.presentation,i=A(r,n);e.patch({presentation:i})},{id:a.id}))});return()=>{r.abortAll(),i()}}function M(e,t){let n=e.selectionSets.find(e=>e.type===`text`);if(!n?.switchingSets?.[0]?.tracks.length)return;let r=n.switchingSets[0].tracks,i=t.includeForcedTracks?r:r.filter(e=>!e.forced);if(i.length===0)return;let{preferredSubtitleLanguage:a,enableDefaultTrack:o=!1}=t;if(a){let e=i.find(e=>e.language===a);if(e)return e.id}if(o){let e=i.find(e=>e.default===!0);if(e)return e.id}}function N(e,t){return!!e?.presentation?.selectionSets?.find(({type:e})=>e===t.type)?.switchingSets?.[0]?.tracks.length}function P(e,t){return!e[c[t.type]]}function Se({state:e},t={type:`video`}){let n=!1;return e.subscribe(async r=>{if(!(!N(r,t)||!P(r,t)||n))try{n=!0;let i=r.presentation?.selectionSets.find(({type:e})=>e===t.type)?.switchingSets[0]?.tracks[0]?.id;if(i){let n=c[t.type];e.patch({[n]:i})}}finally{n=!1}})}function Ce({state:e},t={type:`audio`}){let n=!1;return e.subscribe(async r=>{if(!(!N(r,t)||!P(r,t)||n))try{n=!0;let t=r.presentation?.selectionSets.find(({type:e})=>e===`audio`)?.switchingSets[0]?.tracks[0]?.id;t&&e.patch({selectedAudioTrackId:t})}finally{n=!1}})}function we({state:e},t={type:`text`}){let n=!1;return e.subscribe(async r=>{if(!(!N(r,t)||!P(r,t)||n))try{n=!0;let i=M(r.presentation,t);i&&e.patch({selectedTextTrackId:i})}finally{n=!1}})}const Te=Symbol(`@videojs/spf/state`);function Ee(e,t){return Object.is(e,t)}var De=class{[Te]=!0;#e;#t=null;#n=!1;#r;#i=new Set;#a=new Set;constructor(e,t){this.#e=typeof e==`object`&&e?{...e}:e,this.#r=t?.equalityFn??Ee}get current(){return this.#t??this.#e}patch(e){let t=this.#t??this.#e;if(typeof t!=`object`||!t){let n=e;Object.is(t,n)||(this.#t=n,this.#o());return}let n={...t},r=!1;for(let i in e){if(!Object.hasOwn(e,i))continue;let a=e[i];Object.is(t[i],a)||(n[i]=a,r=!0)}r&&(this.#t=n,this.#o())}subscribe(e,t,n){if(t===void 0){let t=e;return this.#i.add(t),t(this.current),()=>{this.#i.delete(t)}}let r=e,i=t,a={selector:r,listener:i,options:n??{}};return this.#a.add(a),i(r(this.current)),()=>{this.#a.delete(a)}}flush(){if(this.#t===null)return;let e=this.#e,t=this.#t;if(this.#t=null,this.#n=!1,!this.#r(e,t)){this.#e=t;for(let e of this.#i)e(this.#e);for(let t of this.#a){let n=t.selector(e),r=t.selector(this.#e);(t.options.equalityFn??Object.is)(n,r)||t.listener(r)}}}#o(){this.#n||(this.#n=!0,queueMicrotask(()=>this.flush()))}};function F(e,t){return new De(e,t)}function I(e,t){if(e.length===0)return!0;let n=e[e.length-1];return n?t?.snapshot.context.segments.some(e=>e.id===n.id)??!1:!1}function Oe(e,t){let n=e.selectedVideoTrackId?u(e,`video`):void 0,r=e.selectedAudioTrackId?u(e,`audio`):void 0;return!(n&&!o(n)||r&&!o(r)||n&&o(n)&&!I(n.segments,t.videoBufferActor)||r&&o(r)&&!I(r.segments,t.audioBufferActor))}function ke(e,t){return!!(t.mediaSource&&e.presentation)}function Ae(e,t){if(!ke(e,t))return!1;let{mediaSource:n,mediaElement:r}=t;if(n.readyState!==`open`||r&&r.readyState<HTMLMediaElement.HAVE_METADATA)return!1;let i=!!e.selectedVideoTrackId,a=!!e.selectedAudioTrackId;if(i&&!t.videoBuffer||a&&!t.audioBuffer||t.videoBufferActor?.snapshot.status===`updating`||t.audioBufferActor?.snapshot.status===`updating`||!Oe(e,t))return!1;if(r){let t=i?u(e,`video`):void 0,n=a?u(e,`audio`):void 0,s=t&&o(t)?t:n&&o(n)?n:void 0;if(s&&s.segments.length>0){let e=s.segments[s.segments.length-1];if(r.currentTime<e.startTime)return!1}}return!0}function je(e){let t=[e.videoBufferActor,e.audioBufferActor].filter(e=>e!==void 0&&e.snapshot.status===`updating`);return t.length===0?Promise.resolve():Promise.all(t.map(e=>new Promise(t=>{let n=e.subscribe(e=>{e.status!==`updating`&&(n(),t())})}))).then(()=>void 0)}function Me(e){let t=0;for(let n of[e.videoBuffer,e.audioBuffer])if(n&&n.buffered.length>0){let e=n.buffered.end(n.buffered.length-1);e>t&&(t=e)}return t}const Ne=async({currentOwners:e},t)=>{let{mediaSource:n}=e;if(n.readyState===`ended`||(await je(e),n.readyState!==`open`))return;let r=Me(e);r>0&&(n.duration=r),n.endOfStream(),await new Promise(e=>requestAnimationFrame(e))};function Pe({state:e,owners:t}){let n=!1,r=[],i=async()=>{let r=e.current,i=t.current;if(n){if(i.mediaSource?.readyState!==`open`)return;n=!1}if(Ae(r,i)){n=!0;try{await Ne({currentOwners:i},{})}catch(e){console.error(`Failed to call endOfStream:`,e)}}},o=t.subscribe(e=>{r.forEach(e=>e()),r.length=0;for(let t of[e.videoBufferActor,e.audioBufferActor]){if(!t)continue;let e=!0;r.push(t.subscribe(()=>{if(e){e=!1;return}i()}))}}),s=a([e,t]).subscribe(async()=>i());return()=>{r.forEach(e=>e()),o(),s()}}const Fe={keepSegments:2};function Ie(e,t,n=Fe){if(e.length===0)return 0;let r=e.filter(e=>e.startTime<t);if(r.length===0)return 0;let i=r.length-n.keepSegments;return i<=0?0:i>=r.length?t:r[i].startTime}const L={bufferDuration:30};function Le(e,t,n=L){if(e.length===0)return 1/0;let r=t+n.bufferDuration,i=e.filter(e=>e.startTime>=r);return i.length===0?1/0:Math.min(...i.map(e=>e.startTime))}function R(e,t,n,r=L){if(e.length===0)return[];let i=n+r.bufferDuration,a=new Set(t.map(e=>e.startTime));return e.filter(e=>{let t=e.startTime+e.duration,r=e.startTime<i&&t>n,o=!a.has(e.startTime);return r&&o})}function z(){return typeof MediaSource<`u`}function B(){return typeof ManagedMediaSource<`u`}function Re(e={}){let{preferManaged:t=!1}=e;if(t&&B())return new ManagedMediaSource;if(z())return new MediaSource;throw Error(`MediaSource API is not supported`)}function ze(e,t){if(B()&&e instanceof ManagedMediaSource)return t.srcObject=e,{url:``,detach:()=>{t.srcObject=null,t.load()}};let n=URL.createObjectURL(e);return t.src=n,{url:n,detach:()=>{t.removeAttribute(`src`),t.load(),URL.revokeObjectURL(n)}}}function Be(e,t){return new Promise((n,r)=>{if(e.readyState===`open`){n();return}if(t?.aborted){r(new DOMException(`Aborted`,`AbortError`));return}let i=new AbortController,a={signal:i.signal};e.addEventListener(`sourceopen`,()=>{i.abort(),n()},a),t?.addEventListener(`abort`,()=>{i.abort(),r(new DOMException(`Aborted`,`AbortError`))},a)})}function Ve(e,t){if(e.readyState!==`open`)throw Error(`MediaSource is not open`);if(!He(t))throw Error(`Codec not supported: ${t}`);return e.addSourceBuffer(t)}function He(e){return z()?MediaSource.isTypeSupported(e):!1}function Ue(e,t){let n=null,r=null,i=null,a=null,o=!1,s=!1,c=t=>{let n=new Set(e.snapshot.context.segments.map(e=>e.id));return t.filter(e=>n.has(e.id))},l=t=>{let{track:n,range:r}=t,i=e.snapshot.context,a=c(n.segments),o=r?.start??0,s=[];if(r){let e=Le(a,o);e<1/0&&s.push({type:`remove`,start:e,end:1/0});let t=Ie(a,o);t>0&&s.push({type:`remove`,start:0,end:t})}if(i.initTrackId!==n.id&&s.push({type:`append-init`,meta:{trackId:n.id},url:n.initialization.url}),r){let e=R(n.segments,a,o).filter(e=>{let t=i.segments.find(t=>Math.abs(t.startTime-e.startTime)<1e-4);return!t?.trackBandwidth||!n.bandwidth?!0:n.bandwidth>t.trackBandwidth});for(let t of e)s.push({type:`append-segment`,meta:{id:t.id,startTime:t.startTime,duration:t.duration,trackId:n.id,trackBandwidth:n.bandwidth},url:t.url})}return s},u=async o=>{let s=a.signal;try{if(o.type===`remove`){await e.send(o,s);return}if(o.type===`append-init`){if(r=o.meta.trackId,!s.aborted){let r=await t(o,{signal:s}),i=n?.some(e=>e.type===`append-init`&&e.meta.trackId!==o.meta.trackId);if(!s.aborted||!i){let t=s.aborted?new AbortController().signal:s;await e.send({type:`append-init`,data:r,meta:o.meta},t)}}return}if(i=o.meta.id,!s.aborted){let n=await t(o,{signal:s});s.aborted||await e.send({type:`append-segment`,data:n,meta:o.meta},s)}}finally{r=null,i=null}},d=async e=>{o=!0,a=new AbortController;let t=e;for(;t.length>0&&!s;){let e=t[0];t=t.slice(1);try{await u(e)}catch(e){e instanceof Error&&e.name===`AbortError`||(console.error(`Unexpected error in segment loader:`,e),t=[])}if(n!==null)t=n,n=null,a=new AbortController;else if(a.signal.aborted)break}a=null,o=!1};return{send(e){if(s)return;let t=l(e);if(!o){if(t.length===0)return;d(t);return}i!==null&&t.some(e=>e.type===`append-segment`&&e.meta.id===i)||r!==null&&t.some(e=>e.type===`append-init`&&e.meta.trackId===r)?n=t.filter(e=>!(e.type===`append-segment`&&e.meta.id===i)&&!(e.type===`append-init`&&e.meta.trackId===r)):(n=t,a?.abort())},destroy(){s=!0,a?.abort()}}}const We={video:`videoBufferActor`,audio:`audioBufferActor`};function Ge(e,t){return async(n,r)=>{let i=performance.now(),a=await S(n,r),o=performance.now()-i,s=ee(e.current,o,a.byteLength);return e.patch(s),e.flush(),t?.(s),a}}function Ke([e,t],n){let{playbackInitiated:r,preload:i,currentTime:a}=t;return{playbackInitiated:r,preload:i,currentTime:a,track:u(t,n),segmentsCanLoad:e}}const V=(e,t)=>{if(e!=null)return t?.segments.find(({startTime:t,duration:n},r,i)=>e>=t&&(e<t+n||r===i.length-1))?.startTime};function qe(e,t){return t.segmentsCanLoad?t.playbackInitiated?!e.playbackInitiated&&t.playbackInitiated&&e.preload!==`auto`?!1:!t.track||!o(t.track)?!0:e.track?.id!==t.track.id&&o(t.track)?!1:V(e.currentTime,t.track)===V(t.currentTime,t.track):t.preload===`none`?!0:t.preload===e.preload:!0}function H({state:e,owners:t},n){let{type:r}=n,i=We[r],s=e.current.bandwidthState,c=F(s??{fastEstimate:0,fastTotalWeight:0,slowEstimate:0,slowTotalWeight:0,bytesSampled:0}),l=r===`video`?Ge(c,s===void 0?void 0:t=>{e.patch({bandwidthState:t}),e.flush()}):S,d=F(void 0),f=t.subscribe(e=>e[i],e=>(e?d.patch(Ue(e,l)):!e&&d.current&&(d.current.destroy(),d.patch(void 0)),()=>{d.current?.destroy(),d.patch(void 0)})),p=F(!1),m=a([e,d]).subscribe(([e,t])=>{let n=u(e,r),i=!!n&&o(n),a=!!t;p.patch(i&&a)}),h=a([p,e]).subscribe(([e,t])=>Ke([e,t],r),({preload:e,playbackInitiated:t,currentTime:n,track:r})=>{e===`auto`||t?d.current?.send({type:`load`,track:r,range:{start:n,end:n+L.bufferDuration}}):d.current?.send({type:`load`,track:r})},{equalityFn:qe});return()=>{m(),h(),f()}}let U=null;function Je(){return U||(U=document.createElement(`video`),U.muted=!0,U.preload=`none`,U.style.display=`none`,U.crossOrigin=`anonymous`),U}function Ye(e){let t=Je(),n=document.createElement(`track`);return n.kind=`subtitles`,n.default=!0,new Promise((r,i)=>{let a=()=>{let e=[],t=n.track;if(t.cues)for(let n=0;n<t.cues.length;n++){let r=t.cues[n];r&&e.push(r)}s(),r(e)},o=()=>{s(),i(Error(`Failed to load VTT segment: ${e}`))},s=()=>{n.removeEventListener(`load`,a),n.removeEventListener(`error`,o),t.removeChild(n)};n.addEventListener(`load`,a),n.addEventListener(`error`,o),t.appendChild(n),n.src=e})}function Xe(){U=null}function Ze(e,t){let{cues:n}=t;if(!n)return!1;for(let t=0;t<n.length;t++){let r=n[t];if(r.startTime===e.startTime&&r.endTime===e.endTime&&r.text===e.text)return!0}return!1}const Qe=async({segment:e},t)=>{let n=await Ye(e.url);for(let e of n)Ze(e,t.textTrack)||t.textTrack.addCue(e)},$e=async({currentState:e},t)=>{let n=W(e);if(!n||!o(n))return;let{segments:r}=n;if(r.length===0)return;let i=n.id,a=new Set((e.textBufferState?.[i]?.segments??[]).map(e=>e.id)),s=R(r,r.filter(e=>a.has(e.id)),e.currentTime??0).filter(e=>!a.has(e.id));if(s.length!==0){for(let e of s){if(t.signal.aborted)break;try{await Qe({segment:e},{textTrack:t.textTrack});let n=t.state.current.textBufferState??{},r=n[i]??{segments:[]};t.state.patch({textBufferState:{...n,[i]:{segments:[...r.segments,{id:e.id}]}}})}catch(e){if(e instanceof Error&&e.name===`AbortError`)break;console.error(`Failed to load VTT segment:`,e)}}await new Promise(e=>requestAnimationFrame(e))}};function W(e){if(!e.presentation||!e.selectedTextTrackId)return;let t=e.presentation.selectionSets.find(e=>e.type===`text`);if(t?.switchingSets?.[0]?.tracks)return t.switchingSets[0].tracks.find(t=>t.id===e.selectedTextTrackId)}function G(e,t){let n=e.selectedTextTrackId;if(!(!n||!t.textTracks))return t.textTracks.get(n)?.track}function et(e,t){return!!e.selectedTextTrackId&&!!t.textTracks&&t.textTracks.has(e.selectedTextTrackId)}function tt(e,t){if(!et(e,t))return!1;let n=W(e);return!(!n||!o(n)||n.segments.length===0||!G(e,t))}function nt({state:e,owners:t}){let n=null,r=null,i;return a([e,t]).subscribe(async([t,a])=>{if(t.selectedTextTrackId!==i&&(i=t.selectedTextTrackId,r?.abort(),n=null),n||!tt(t,a))return;let o=G(t,a);o&&(r=new AbortController,n=$e({currentState:t},{signal:r.signal,textTrack:o,state:e}).finally(()=>{n=null}))})}function rt(t,n){return!e(n.mediaElement)&&!e(t.presentation?.url)}function it(t,n){return e(n.mediaSource)}function at({state:e,owners:t}){let n=!1;return a([e,t]).subscribe(async([e,r])=>{if(!(!rt(e,r)||!it(e,r)||n))try{n=!0;let e=Re({preferManaged:!0});ze(e,r.mediaElement),await Be(e),t.patch({mediaSource:e})}finally{n=!1}})}async function K(e,t){return e.updating&&await new Promise(t=>{let n=()=>{e.removeEventListener(`updateend`,n),t()};e.addEventListener(`updateend`,n)}),new Promise((n,r)=>{let i=()=>{o(),n()},a=e=>{o(),r(Error(`SourceBuffer append error: ${e.type}`))},o=()=>{e.removeEventListener(`updateend`,i),e.removeEventListener(`error`,a)};e.addEventListener(`updateend`,i),e.addEventListener(`error`,a);try{e.appendBuffer(t)}catch(e){o(),r(e)}})}async function ot(e,t,n){return e.updating&&await new Promise(t=>{let n=()=>{e.removeEventListener(`updateend`,n),t()};e.addEventListener(`updateend`,n)}),new Promise((r,i)=>{let a=()=>{s(),r()},o=e=>{s(),i(Error(`SourceBuffer remove error: ${e.type}`))},s=()=>{e.removeEventListener(`updateend`,a),e.removeEventListener(`error`,o)};e.addEventListener(`updateend`,a),e.addEventListener(`error`,o);try{e.remove(t,n)}catch(e){s(),i(e)}})}var q=class extends Error{constructor(e){super(e),this.name=`SourceBufferActorError`}};function J(e){let t=[];for(let n=0;n<e.length;n++)t.push({start:e.start(n),end:e.end(n)});return t}function st(e,{signal:t,getCtx:n,sourceBuffer:r}){return new k(async t=>{let i=n();return t.aborted?i:(await K(r,e.data),{...i,initTrackId:e.meta.trackId})},{signal:t})}function ct(e,{signal:t,getCtx:n,sourceBuffer:r}){return new k(async t=>{let i=n();if(t.aborted)return i;await K(r,e.data);let{meta:a}=e,o=i.segments.filter(e=>Math.abs(e.startTime-a.startTime)>=1e-4);return{...i,segments:[...o,{id:a.id,startTime:a.startTime,duration:a.duration,trackId:a.trackId,...a.trackBandwidth!==void 0&&{trackBandwidth:a.trackBandwidth}}],bufferedRanges:J(r.buffered)}},{signal:t})}function lt(e,{signal:t,getCtx:n,sourceBuffer:r}){return new k(async t=>{let i=n();if(t.aborted)return i;await ot(r,e.start,e.end);let a=J(r.buffered),o=i.segments.filter(e=>{let t=e.startTime+e.duration/2;return a.some(e=>t>=e.start&&t<e.end)});return{...i,segments:o,bufferedRanges:a}},{signal:t})}const ut={"append-init":st,"append-segment":ct,remove:lt};function Y(e,t){let n=ut[e.type];return n(e,t)}function dt(e,t){let n=F({status:`idle`,context:{segments:[],bufferedRanges:[],initTrackId:void 0,...t}}),r=new ye;function i(e){let t=n.current.status===`destroyed`?`destroyed`:`idle`;n.patch({status:t,context:e}),n.flush()}function a(e){let t=n.current.status===`destroyed`?`destroyed`:`idle`;throw n.patch({status:t}),n.flush(),e}return{get snapshot(){return n.current},subscribe(e){return n.subscribe(e)},send(t,o){if(n.current.status!==`idle`)return Promise.reject(new q(`send() called while actor is ${n.current.status}`));n.patch({status:`updating`});let s=Y(t,{signal:o,getCtx:()=>n.current.context,sourceBuffer:e});return r.schedule(s).then(i).catch(a)},batch(t,o){if(n.current.status!==`idle`)return Promise.reject(new q(`batch() called while actor is ${n.current.status}`));if(t.length===0)return Promise.resolve();n.patch({status:`updating`});let s=n.current.context;for(let n of t.slice(0,-1)){let t=Y(n,{signal:o,getCtx:()=>s,sourceBuffer:e});r.schedule(t).then(e=>{s=e})}let c=Y(t[t.length-1],{signal:o,getCtx:()=>s,sourceBuffer:e});return r.schedule(c).then(i).catch(a)},destroy(){n.patch({status:`destroyed`}),n.flush(),r.destroy()}}}const ft={video:`videoBufferActor`,audio:`audioBufferActor`},pt=async({currentState:e,currentOwners:t},n)=>{let r=u(e,n.config.type);if(!r||!o(r)||!r.codecs||r.codecs.length===0)return;let i=mt(r),a=Ve(t.mediaSource,i),s=dt(a),c=l[n.config.type],d=ft[n.config.type];n.owners.patch({[c]:a,[d]:s}),await new Promise(e=>requestAnimationFrame(e))};function mt(e){let t=e.codecs?.join(`,`)??``;return`${e.mimeType}; codecs="${t}"`}function ht(e,t,n){return!(!t.mediaSource||!u(e,n))}function gt(e,n){let r=l[n];return t(e[r])}function X({state:e,owners:t},n){let r=null;return a([e,t]).subscribe(async([e,i])=>{if(!r&&!(!ht(e,i,n.type)||!gt(i,n.type))){r=pt({currentState:e,currentOwners:i},{owners:t,config:n});try{await r}finally{r=null}}})}function Z(e){if(!e?.selectionSets)return[];let t=e.selectionSets.find(e=>e.type===`text`);return t?.switchingSets?.[0]?.tracks?t.switchingSets[0].tracks:[]}function _t(e,t){return!!t.mediaElement&&!!Z(e.presentation).length}function vt(e){return!e.textTracks}function yt(e){let t=document.createElement(`track`);return t.id=e.id,t.kind=e.kind,t.label=e.label,e.language&&(t.srclang=e.language),e.default&&(t.default=!0),t.src=e.url,t}function bt({state:e,owners:t}){let n=!1,r=[],i=a([e,t]).subscribe(([e,i])=>{if(n||!_t(e,i)||!vt(i))return;n=!0;let a=Z(e.presentation);if(a.length===0)return;let o=new Map;for(let e of a){let t=yt(e);i.mediaElement.appendChild(t),o.set(e.id,t),r.push(t)}t.patch({textTracks:o})});return()=>{for(let e of r)e.remove();r=[],i()}}function Q(e){return!!e.textTracks&&e.textTracks.size>0}function xt({state:e,owners:t}){return a([e,t]).subscribe(([e,t])=>{if(!Q(t))return;let n=e.selectedTextTrackId;for(let[e,r]of t.textTracks)e===n?r.track.mode=`showing`:r.track.mode=`hidden`})}function St({state:e,owners:t}){let r,i=null,a=t.subscribe(t=>{let{mediaElement:a}=t;if(a===r||(i?.(),i=null,r=a,!a))return;e.patch({currentTime:a.currentTime});let o=()=>e.patch({currentTime:a.currentTime}),s=n(a,`timeupdate`,o),c=n(a,`seeking`,o);i=()=>{s(),c()}});return()=>{i?.(),a()}}function Ct({state:e,owners:t,events:r}){let i,a=null,o,s=e.subscribe(t=>{let n=t.presentation?.url;n!==o&&(o!==void 0&&e.patch({playbackInitiated:!1}),o=n)}),c=t.subscribe(t=>{let{mediaElement:o}=t;o!==i&&(a?.(),a=null,i=o,o&&(a=n(o,`play`,()=>{e.patch({playbackInitiated:!0}),r.dispatch({type:`play`})})))});return()=>{a?.(),s(),c()}}function wt(e,t){return!!(t.mediaSource&&e.presentation&&s(e.presentation))}function Tt(e){let t=0,n=[e.videoSourceBuffer,e.audioSourceBuffer].filter(e=>e!==void 0);for(let e of n){let{buffered:n}=e;if(n.length>0){let e=n.end(n.length-1);e>t&&(t=e)}}return t}function Et(e,t){if(!wt(e,t))return!1;let{mediaSource:n}=t,{presentation:r}=e;if(n.readyState!==`open`)return!1;let i=r.duration;return!Number.isFinite(i)||Number.isNaN(i)||i<=0?!1:isNaN(n.duration)}function Dt(e){let t=[e.videoSourceBuffer,e.audioSourceBuffer].filter(e=>e!==void 0&&e.updating);return t.length===0?Promise.resolve():Promise.all(t.map(e=>new Promise(t=>e.addEventListener(`updateend`,()=>t(),{once:!0})))).then(()=>void 0)}function Ot({state:e,owners:t}){return a([e,t]).subscribe(async([e,t])=>{if(!Et(e,t))return;let{mediaSource:n}=t;if(await Dt(t),n.readyState!==`open`)return;let r=e.presentation.duration,i=Tt(t);i>r&&(r=i),n.duration=r})}function $(e={}){let t=F({bandwidthState:{fastEstimate:0,fastTotalWeight:0,slowEstimate:0,slowTotalWeight:0,bytesSampled:0}}),n=F({}),r=i(),a=[he(t,n),Ct({state:t,owners:n,events:r}),ge({state:t,events:r}),Se({state:t,owners:n,events:r},{type:`video`,...e.initialBandwidth!==void 0&&{initialBandwidth:e.initialBandwidth}}),Ce({state:t,owners:n,events:r},{type:`audio`,...e.preferredAudioLanguage!==void 0&&{preferredAudioLanguage:e.preferredAudioLanguage}}),we({state:t,owners:n,events:r},{type:`text`,...e.preferredSubtitleLanguage!==void 0&&{preferredSubtitleLanguage:e.preferredSubtitleLanguage},...e.includeForcedTracks!==void 0&&{includeForcedTracks:e.includeForcedTracks},...e.enableDefaultTrack!==void 0&&{enableDefaultTrack:e.enableDefaultTrack}}),j({state:t,events:r},{type:`video`}),j({state:t,events:r},{type:`audio`}),j({state:t,events:r},{type:`text`}),m({state:t}),at({state:t,owners:n}),Ot({state:t,owners:n}),X({state:t,owners:n},{type:`video`}),X({state:t,owners:n},{type:`audio`}),St({state:t,owners:n}),ae({state:t}),H({state:t,owners:n},{type:`video`}),H({state:t,owners:n},{type:`audio`}),Pe({state:t,owners:n}),bt({state:t,owners:n}),xt({state:t,owners:n}),nt({state:t,owners:n})];return r.dispatch({type:`@@INITIALIZE@@`}),{state:t,owners:n,events:r,destroy:()=>{a.forEach(e=>e()),Xe()}}}var kt=class{engine;#e=null;constructor(e={}){this.engine=$(e)}attach(e){this.engine.owners.patch({mediaElement:e})}detach(){this.#t(),this.engine.owners.patch({mediaElement:void 0})}destroy(){this.#t(),this.engine.destroy()}get src(){return this.engine.state.current.presentation?.url??``}set src(e){this.engine.state.patch({presentation:e?{url:e}:void 0})}play(){let{mediaElement:e}=this.engine.owners.current;return e?(this.engine.state.patch({playbackInitiated:!0}),e.play().catch(t=>{if(this.src)return new Promise((t,n)=>{let r=()=>{this.#e=null,e.play().then(t,n)};this.#e=r,e.addEventListener(`loadstart`,r,{once:!0})});throw t})):Promise.reject(Error(`SpfMedia: no media element attached`))}#t(){if(!this.#e)return;let{mediaElement:e}=this.engine.owners.current;e?.removeEventListener(`loadstart`,this.#e),this.#e=null}};export{kt as SpfMedia,$ as createPlaybackEngine};
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@videojs/spf",
3
+ "type": "module",
4
+ "version": "0.1.0-alpha.2",
5
+ "description": "Stream Processing Framework for Video.js 10",
6
+ "license": "Apache-2.0",
7
+ "keywords": [
8
+ "video",
9
+ "hls",
10
+ "streaming",
11
+ "spf",
12
+ "videojs"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "development": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./dom": {
21
+ "types": "./dist/dom/index.d.ts",
22
+ "development": "./dist/dom/index.js",
23
+ "default": "./dist/dom/index.js"
24
+ },
25
+ "./playback-engine": {
26
+ "types": "./dist/dom/playback-engine/index.d.ts",
27
+ "development": "./dist/dom/playback-engine/index.js",
28
+ "default": "./dist/dom/playback-engine/index.js"
29
+ }
30
+ },
31
+ "main": "dist/index.js",
32
+ "module": "dist/index.js",
33
+ "types": "dist/index.d.ts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc --build && tsdown",
39
+ "build:watch": "tsdown --watch --silent",
40
+ "dev": "pnpm run build:watch",
41
+ "test": "vitest run",
42
+ "test:ci": "vitest run --project=core --project=dom --project=utils",
43
+ "test:watch": "vitest",
44
+ "clean": "rm -rf dist types",
45
+ "size": "node scripts/measure-size.js --playback-engine",
46
+ "size:all": "node scripts/measure-size.js --all"
47
+ },
48
+ "dependencies": {
49
+ "@videojs/utils": "workspace:*"
50
+ },
51
+ "devDependencies": {
52
+ "@vitest/browser": "^3.2.4",
53
+ "@vitest/coverage-v8": "^3.2.4",
54
+ "jsdom": "^26.1.0",
55
+ "playwright": "^1.58.1",
56
+ "tsdown": "^0.15.12",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^3.2.4"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }