@stacksjs/video 0.75.514 → 0.75.518

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/files.js CHANGED
@@ -1 +1 @@
1
- import{videoAssetHeaders}from"./index";export function parseByteRange(header,size){const match=/^bytes=(\d*)-(\d*)$/.exec(String(header??"").trim());if(!match||!match[1]&&!match[2])return null;if(!match[1]){const length=Math.min(Number(match[2]),size);return length>0?{start:size-length,end:size-1}:"unsatisfiable"}const start=Number(match[1]);if(start>=size)return"unsatisfiable";const end=match[2]?Math.min(Number(match[2]),size-1):size-1;return end>=start?{start,end}:"unsatisfiable"}const VIDEO_TYPES={mp4:"video/mp4",m4v:"video/mp4",mov:"video/quicktime",webm:"video/webm",m3u8:"application/vnd.apple.mpegurl",mpd:"application/dash+xml",m4s:"video/iso.segment",vtt:"text/vtt"};export function videoContentType(name){return VIDEO_TYPES[name.split(/[?#]/,1)[0].split(".").pop()?.toLowerCase()||""]||"application/octet-stream"}export function videoFileResponse(file,name,options={}){const size=file.size,type=options.contentType||videoContentType(name),range=parseByteRange(options.range,size);if(range==="unsatisfiable")return new Response(null,{status:416,headers:{"Content-Range":`bytes */${size}`}});if(!range)return new Response(file,{status:200,headers:{...videoAssetHeaders(name,size,options.etag,options.protectedMedia),"Content-Type":type}});const length=range.end-range.start+1;return new Response(file.slice(range.start,range.end+1),{status:206,headers:{...videoAssetHeaders(name,length,options.etag,options.protectedMedia),"Content-Type":type,"Content-Range":`bytes ${range.start}-${range.end}/${size}`}})}
1
+ import{videoAssetHeaders}from"./index";export function parseByteRange(header,size){const match=/^bytes=(\d*)-(\d*)$/.exec(String(header??"").trim());if(!match||!match[1]&&!match[2])return null;if(!match[1]){const length=Math.min(Number(match[2]),size);return length>0?{start:size-length,end:size-1}:"unsatisfiable"}const start=Number(match[1]);if(start>=size)return"unsatisfiable";const end=match[2]?Math.min(Number(match[2]),size-1):size-1;return end>=start?{start,end}:"unsatisfiable"}const VIDEO_TYPES={mp4:"video/mp4",m4v:"video/mp4",mov:"video/quicktime",webm:"video/webm",m3u8:"application/vnd.apple.mpegurl",mpd:"application/dash+xml",m4s:"video/iso.segment",vtt:"text/vtt"};export function videoContentType(name){return VIDEO_TYPES[name.split(/[?#]/,1)[0].split(".").pop()?.toLowerCase()||""]||"application/octet-stream"}export function videoFileResponse(file,name,options={}){const size=file.size;const type=options.contentType||videoContentType(name);const range=parseByteRange(options.range,size);if(range==="unsatisfiable")return new Response(null,{status:416,headers:{"Content-Range":`bytes */${size}`}});if(!range)return new Response(file,{status:200,headers:{...videoAssetHeaders(name,size,options.etag,options.protectedMedia),"Content-Type":type}});const length=range.end-range.start+1;return new Response(file.slice(range.start,range.end+1),{status:206,headers:{...videoAssetHeaders(name,length,options.etag,options.protectedMedia),"Content-Type":type,"Content-Range":`bytes ${range.start}-${range.end}/${size}`}})}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import{createHmac,timingSafeEqual}from"node:crypto";const edges=[240,360,480,540,720,1080,1440,2160],even=(value)=>Math.max(2,Math.round(value/2)*2);function validate(profile){for(const[name,value]of Object.entries({width:profile.width,height:profile.height,duration:profile.duration,frameRate:profile.frameRate}))if(!Number.isFinite(value)||value<=0)throw TypeError(`Video ${name} must be positive`)}export function deriveVideoLadder(profile,maximum){validate(profile);const short=Math.min(profile.width,profile.height),limit=Math.min(short,maximum??short),targets=edges.filter((edge)=>edge<=limit);if(!targets.includes(limit))targets.push(limit);return[...new Set(targets)].sort((a,b)=>a-b).map((edge)=>{const scale=edge/short,width=even(profile.width*scale),height=even(profile.height*scale),rate=Math.round(width*height*Math.min(60,profile.frameRate)*0.075*(profile.frameRate>30?Math.min(2,profile.frameRate/30):1)*(profile.hdr?1.25:1)/1000)*1000;return{name:`${edge}p`,width,height,frameRate:profile.frameRate,videoBitrate:Math.max(250000,Math.min(profile.videoBitrate??1/0,rate)),audioBitrate:profile.hasAudio===!1?0:width>=1280?192000:128000}}).filter((item,index,all)=>item.width<=profile.width&&item.height<=profile.height&&all.findIndex((value)=>value.width===item.width&&value.height===item.height)===index)}export class VideoBuilder{source;inspected;formats=["mp4","webm"];streams=["hls","dash"];maximum;capabilities={videoEncoder:!1,audioEncoder:!1,videoCodecs:[],audioCodecs:[]};constructor(source){this.source=source}profile(value){validate(value);this.inspected=value;return this}ladder(value){if(value!=="auto"&&(!Number.isInteger(value)||value<=0))throw TypeError("Video ladder height must be positive");this.maximum=value==="auto"?void 0:value;return this}output(formats){if(!formats.length)throw TypeError("Video formats are required");this.formats=[...new Set(formats)];return this}streaming(formats){this.streams=[...new Set(formats)];return this}runtime(value){this.capabilities=value;return this}generate(){if(!this.inspected)throw Error("Video inspection is required; pass its profile with .profile()");const profile=this.inspected,renditions=deriveVideoLadder(profile,this.maximum),outputs=this.formats.map((container)=>{const videoCodec=container==="mp4"?"h264":"vp9",audioCodec=profile.hasAudio===!1?void 0:container==="mp4"?"aac":"opus",copy=renditions.length===1&&renditions[0].width===profile.width&&renditions[0].height===profile.height&&profile.container===container&&profile.videoCodec===videoCodec&&(!audioCodec||profile.audioCodec===audioCodec),available=copy||this.capabilities.videoEncoder&&this.capabilities.videoCodecs.includes(videoCodec)&&(!audioCodec||this.capabilities.audioEncoder&&this.capabilities.audioCodecs.includes(audioCodec));return{container,videoCodec,audioCodec,action:copy?"copy":"transcode",available,reason:available?void 0:`Native ${videoCodec}${audioCodec?`/${audioCodec}`:""} encoding is unavailable`}}),segmentDuration=profile.duration<=30?2:profile.duration<=600?4:6;return{source:this.source,profile,renditions,outputs,streaming:this.streams,segmentDuration,keyframeInterval:Math.max(1,Math.round(profile.frameRate*segmentDuration))}}async process(options={}){return processVideoPlan(this.generate(),options)}}export function video(source){return new VideoBuilder(source)}export async function processVideoPlan(plan,options={}){assertVideoPlanExecutable(plan);const{createVideoDeliveryPipeline}=await import("ts-videos/delivery-pipeline");return createVideoDeliveryPipeline(plan.source,{source:plan.profile,renditions:plan.renditions,outputs:plan.outputs,streaming:plan.streaming,segmentDuration:plan.segmentDuration,keyframeInterval:plan.keyframeInterval},options)}export function assertVideoPlanExecutable(plan){const missing=plan.outputs.filter((value)=>!value.available);if(missing.length)throw Error(missing.map((value)=>`${value.container}: ${value.reason}`).join("; "))}export function createHlsMaster(plan,uri){const lines=["#EXTM3U","#EXT-X-VERSION:7","#EXT-X-INDEPENDENT-SEGMENTS"],output=plan.outputs.find((value)=>value.container==="mp4");for(const item of plan.renditions){lines.push(`#EXT-X-STREAM-INF:BANDWIDTH=${item.videoBitrate+item.audioBitrate},AVERAGE-BANDWIDTH=${Math.round((item.videoBitrate+item.audioBitrate)*0.9)},RESOLUTION=${item.width}x${item.height},FRAME-RATE=${item.frameRate.toFixed(3)},CODECS="${output?.videoCodec??plan.profile.videoCodec}${output?.audioCodec?`,${output.audioCodec}`:""}"`);lines.push(uri(item))}return`${lines.join(`
1
+ import{createHmac,timingSafeEqual}from"node:crypto";const edges=[240,360,480,540,720,1080,1440,2160];const even=(value)=>Math.max(2,Math.round(value/2)*2);function validate(profile){for(const[name,value]of Object.entries({width:profile.width,height:profile.height,duration:profile.duration,frameRate:profile.frameRate})){if(!Number.isFinite(value)||value<=0)throw new TypeError(`Video ${name} must be positive`)}}export function deriveVideoLadder(profile,maximum){validate(profile);const short=Math.min(profile.width,profile.height);const limit=Math.min(short,maximum??short);const targets=edges.filter((edge)=>edge<=limit);if(!targets.includes(limit))targets.push(limit);return[...new Set(targets)].sort((a,b)=>a-b).map((edge)=>{const scale=edge/short;const width=even(profile.width*scale);const height=even(profile.height*scale);const rate=Math.round(width*height*Math.min(60,profile.frameRate)*0.075*(profile.frameRate>30?Math.min(2,profile.frameRate/30):1)*(profile.hdr?1.25:1)/1000)*1000;return{name:`${edge}p`,width,height,frameRate:profile.frameRate,videoBitrate:Math.max(250000,Math.min(profile.videoBitrate??1/0,rate)),audioBitrate:profile.hasAudio===false?0:width>=1280?192000:128000}}).filter((item,index,all)=>item.width<=profile.width&&item.height<=profile.height&&all.findIndex((value)=>value.width===item.width&&value.height===item.height)===index)}export class VideoBuilder{source;inspected;formats=["mp4","webm"];streams=["hls","dash"];maximum;capabilities={videoEncoder:false,audioEncoder:false,videoCodecs:[],audioCodecs:[]};constructor(source){this.source=source}profile(value){validate(value);this.inspected=value;return this}ladder(value){if(value!=="auto"&&(!Number.isInteger(value)||value<=0))throw new TypeError("Video ladder height must be positive");this.maximum=value==="auto"?undefined:value;return this}output(formats){if(!formats.length)throw new TypeError("Video formats are required");this.formats=[...new Set(formats)];return this}streaming(formats){this.streams=[...new Set(formats)];return this}runtime(value){this.capabilities=value;return this}generate(){if(!this.inspected)throw new Error("Video inspection is required; pass its profile with .profile()");const profile=this.inspected;const renditions=deriveVideoLadder(profile,this.maximum);const outputs=this.formats.map((container)=>{const videoCodec=container==="mp4"?"h264":"vp9";const audioCodec=profile.hasAudio===false?undefined:container==="mp4"?"aac":"opus";const original=renditions.length===1&&renditions[0].width===profile.width&&renditions[0].height===profile.height;const copy=original&&profile.container===container&&profile.videoCodec===videoCodec&&(!audioCodec||profile.audioCodec===audioCodec);const available=copy||this.capabilities.videoEncoder&&this.capabilities.videoCodecs.includes(videoCodec)&&(!audioCodec||this.capabilities.audioEncoder&&this.capabilities.audioCodecs.includes(audioCodec));return{container,videoCodec,audioCodec,action:copy?"copy":"transcode",available,reason:available?undefined:`Native ${videoCodec}${audioCodec?`/${audioCodec}`:""} encoding is unavailable`}});const segmentDuration=profile.duration<=30?2:profile.duration<=600?4:6;return{source:this.source,profile,renditions,outputs,streaming:this.streams,segmentDuration,keyframeInterval:Math.max(1,Math.round(profile.frameRate*segmentDuration))}}async process(options={}){return processVideoPlan(this.generate(),options)}}export function video(source){return new VideoBuilder(source)}export async function processVideoPlan(plan,options={}){assertVideoPlanExecutable(plan);const{createVideoDeliveryPipeline}=await import("ts-videos/delivery-pipeline");return createVideoDeliveryPipeline(plan.source,{source:plan.profile,renditions:plan.renditions,outputs:plan.outputs,streaming:plan.streaming,segmentDuration:plan.segmentDuration,keyframeInterval:plan.keyframeInterval},options)}export function assertVideoPlanExecutable(plan){const missing=plan.outputs.filter((value)=>!value.available);if(missing.length)throw new Error(missing.map((value)=>`${value.container}: ${value.reason}`).join("; "))}export function createHlsMaster(plan,uri){const lines=["#EXTM3U","#EXT-X-VERSION:7","#EXT-X-INDEPENDENT-SEGMENTS"];const output=plan.outputs.find((value)=>value.container==="mp4");for(const item of plan.renditions){lines.push(`#EXT-X-STREAM-INF:BANDWIDTH=${item.videoBitrate+item.audioBitrate},AVERAGE-BANDWIDTH=${Math.round((item.videoBitrate+item.audioBitrate)*0.9)},RESOLUTION=${item.width}x${item.height},FRAME-RATE=${item.frameRate.toFixed(3)},CODECS="${output?.videoCodec??plan.profile.videoCodec}${output?.audioCodec?`,${output.audioCodec}`:""}"`);lines.push(uri(item))}return`${lines.join(`
2
2
  `)}
3
- `}function time(seconds){const ms=Math.round(seconds*1000);return`${String(Math.floor(ms/3600000)).padStart(2,"0")}:${String(Math.floor(ms%3600000/60000)).padStart(2,"0")}:${String(Math.floor(ms%60000/1000)).padStart(2,"0")}.${String(ms%1000).padStart(3,"0")}`}export function createPreviewVtt(cues){let end=0;const lines=["WEBVTT",""];cues.forEach((cue,index)=>{if(cue.startTime<end||cue.endTime<=cue.startTime)throw TypeError(`Invalid preview cue ${index}`);const sprite=[cue.x,cue.y,cue.width,cue.height].every((value)=>value!==void 0);lines.push(`${time(cue.startTime)} --> ${time(cue.endTime)}`,sprite?`${cue.uri}#xywh=${cue.x},${cue.y},${cue.width},${cue.height}`:cue.uri,"");end=cue.endTime});return lines.join(`
3
+ `}function time(seconds){const ms=Math.round(seconds*1000);return`${String(Math.floor(ms/3600000)).padStart(2,"0")}:${String(Math.floor(ms%3600000/60000)).padStart(2,"0")}:${String(Math.floor(ms%60000/1000)).padStart(2,"0")}.${String(ms%1000).padStart(3,"0")}`}export function createPreviewVtt(cues){let end=0;const lines=["WEBVTT",""];cues.forEach((cue,index)=>{if(cue.startTime<end||cue.endTime<=cue.startTime)throw new TypeError(`Invalid preview cue ${index}`);const sprite=[cue.x,cue.y,cue.width,cue.height].every((value)=>value!==undefined);lines.push(`${time(cue.startTime)} --> ${time(cue.endTime)}`,sprite?`${cue.uri}#xywh=${cue.x},${cue.y},${cue.width},${cue.height}`:cue.uri,"");end=cue.endTime});return lines.join(`
4
4
  `)}export function videoResponseHeaders(bytes,etag,contentType){return{"Accept-Ranges":"bytes","Content-Length":String(bytes),"Content-Type":contentType,"Cache-Control":"public, max-age=31536000, immutable",ETag:`"${etag}"`}}export function signVideoAsset(path,expires,secret){return createHmac("sha256",secret).update(`${path}
5
- ${expires}`).digest("base64url")}export function verifyVideoAsset(path,expires,signature,secret,now=Date.now()){if(!Number.isInteger(expires)||expires*1000<=now)return!1;const expected=Buffer.from(signVideoAsset(path,expires,secret)),actual=Buffer.from(signature);return expected.length===actual.length&&timingSafeEqual(expected,actual)}function sequenceIv(index){const value=new Uint8Array(16);new DataView(value.buffer).setBigUint64(8,BigInt(index));return value}function hex(value){return[...value].map((byte)=>byte.toString(16).padStart(2,"0")).join("")}async function encryptAes(data,key,iv){if(key.byteLength!==16||iv.byteLength!==16)throw TypeError("HLS AES-128 keys and IVs must contain 16 bytes");const keyBytes=Uint8Array.from(key),ivBytes=Uint8Array.from(iv),dataBytes=Uint8Array.from(data),cryptoKey=await crypto.subtle.importKey("raw",keyBytes.buffer,{name:"AES-CBC"},!1,["encrypt"]);return new Uint8Array(await crypto.subtle.encrypt({name:"AES-CBC",iv:ivBytes.buffer},cryptoKey,dataBytes.buffer))}export async function createProtectedVideoPlaylist(segments,protection){if(!segments.length)throw TypeError("Video playlist requires segments");if(protection&&/[\r\n"]/.test(protection.keyUri))throw TypeError("Invalid video key URI");const lines=["#EXTM3U","#EXT-X-VERSION:7",`#EXT-X-TARGETDURATION:${Math.ceil(Math.max(...segments.map((item)=>item.duration)))}`,"#EXT-X-PLAYLIST-TYPE:VOD","#EXT-X-INDEPENDENT-SEGMENTS"],files={};for(const[index,segment]of segments.entries()){if(!Number.isFinite(segment.duration)||segment.duration<=0||/[\r\n"]/.test(segment.uri))throw TypeError(`Invalid video segment ${index}`);let data=segment.data;if(protection){const iv=protection.iv?.(index)??sequenceIv(index);data=await encryptAes(data,protection.key,iv);lines.push(`#EXT-X-KEY:METHOD=AES-128,URI="${protection.keyUri}",IV=0x${hex(iv)}`)}lines.push(`#EXTINF:${segment.duration.toFixed(6)},`,segment.uri);files[segment.uri]=data}lines.push("#EXT-X-ENDLIST","");return{playlist:lines.join(`
6
- `),files,encrypted:!!protection}}export function videoAssetHeaders(path,bytes,etag,protectedMedia=!1){const manifest=/\.(?:m3u8|mpd|vtt)$/i.test(path),headers={"Accept-Ranges":"bytes","Cache-Control":protectedMedia&&manifest?"private, no-store":manifest?"public, max-age=5, s-maxage=30":"public, max-age=31536000, immutable","X-Content-Type-Options":"nosniff"};if(bytes!==void 0)headers["Content-Length"]=String(bytes);if(etag)headers.ETag=`"${etag}"`;return headers}export{parseByteRange,videoContentType,videoFileResponse}from"./files";export{inspectVideo}from"./inspect";
5
+ ${expires}`).digest("base64url")}export function verifyVideoAsset(path,expires,signature,secret,now=Date.now()){if(!Number.isInteger(expires)||expires*1000<=now)return false;const expected=Buffer.from(signVideoAsset(path,expires,secret));const actual=Buffer.from(signature);return expected.length===actual.length&&timingSafeEqual(expected,actual)}function sequenceIv(index){const value=new Uint8Array(16);new DataView(value.buffer).setBigUint64(8,BigInt(index));return value}function hex(value){return[...value].map((byte)=>byte.toString(16).padStart(2,"0")).join("")}async function encryptAes(data,key,iv){if(key.byteLength!==16||iv.byteLength!==16)throw new TypeError("HLS AES-128 keys and IVs must contain 16 bytes");const keyBytes=Uint8Array.from(key);const ivBytes=Uint8Array.from(iv);const dataBytes=Uint8Array.from(data);const cryptoKey=await crypto.subtle.importKey("raw",keyBytes.buffer,{name:"AES-CBC"},false,["encrypt"]);return new Uint8Array(await crypto.subtle.encrypt({name:"AES-CBC",iv:ivBytes.buffer},cryptoKey,dataBytes.buffer))}export async function createProtectedVideoPlaylist(segments,protection){if(!segments.length)throw new TypeError("Video playlist requires segments");if(protection&&/[\r\n"]/.test(protection.keyUri))throw new TypeError("Invalid video key URI");const lines=["#EXTM3U","#EXT-X-VERSION:7",`#EXT-X-TARGETDURATION:${Math.ceil(Math.max(...segments.map((item)=>item.duration)))}`,"#EXT-X-PLAYLIST-TYPE:VOD","#EXT-X-INDEPENDENT-SEGMENTS"];const files={};for(const[index,segment]of segments.entries()){if(!Number.isFinite(segment.duration)||segment.duration<=0||/[\r\n"]/.test(segment.uri))throw new TypeError(`Invalid video segment ${index}`);let data=segment.data;if(protection){const iv=protection.iv?.(index)??sequenceIv(index);data=await encryptAes(data,protection.key,iv);lines.push(`#EXT-X-KEY:METHOD=AES-128,URI="${protection.keyUri}",IV=0x${hex(iv)}`)}lines.push(`#EXTINF:${segment.duration.toFixed(6)},`,segment.uri);files[segment.uri]=data}lines.push("#EXT-X-ENDLIST","");return{playlist:lines.join(`
6
+ `),files,encrypted:!!protection}}export function videoAssetHeaders(path,bytes,etag,protectedMedia=false){const manifest=/\.(?:m3u8|mpd|vtt)$/i.test(path);const headers={"Accept-Ranges":"bytes","Cache-Control":protectedMedia&&manifest?"private, no-store":manifest?"public, max-age=5, s-maxage=30":"public, max-age=31536000, immutable","X-Content-Type-Options":"nosniff"};if(bytes!==undefined)headers["Content-Length"]=String(bytes);if(etag)headers.ETag=`"${etag}"`;return headers}export{parseByteRange,videoContentType,videoFileResponse}from"./files";export{inspectVideo}from"./inspect";
package/dist/inspect.js CHANGED
@@ -1 +1 @@
1
- import{Mp4InputFormat,MovInputFormat}from"@ts-videos/mp4";import{WebmInputFormat}from"@ts-videos/webm";import{BufferSource,Input}from"ts-videos";const VIDEO_CODECS={h264:"h264",avc:"h264",avc1:"h264",h265:"h265",hevc:"h265",hvc1:"h265",hev1:"h265",vp8:"vp8",vp9:"vp9",av1:"av1",prores:"prores"},AUDIO_CODECS={aac:"aac",mp3:"mp3",opus:"opus",vorbis:"vorbis",flac:"flac",alac:"alac",ac3:"ac3",eac3:"eac3"};export async function inspectVideo(bytes){const data=bytes instanceof Uint8Array?bytes:new Uint8Array(bytes),input=new Input({source:new BufferSource(data),formats:[new Mp4InputFormat,new MovInputFormat,new WebmInputFormat]});try{const track=await input.getPrimaryVideoTrack();if(!track)return null;const audio=await input.getPrimaryAudioTrack().catch(()=>null),format=await input.getFormatName().catch(()=>""),duration=await input.getDuration().catch(()=>Number.NaN);return{format,container:format==="webm"?"webm":"mp4",width:track.displayWidth||track.width,height:track.displayHeight||track.height,duration:Number.isFinite(duration)&&duration>0?Math.round(duration*1000)/1000:0,frameRate:track.frameRate&&track.frameRate>0?track.frameRate:30,videoCodec:VIDEO_CODECS[String(track.codec).toLowerCase()]??"unknown",...audio?{audioCodec:AUDIO_CODECS[String(audio.codec).toLowerCase()]??"unknown"}:{},hasAudio:!!audio}}catch{return null}finally{await input.close().catch(()=>{})}}
1
+ import{Mp4InputFormat,MovInputFormat}from"@ts-videos/mp4";import{WebmInputFormat}from"@ts-videos/webm";import{BufferSource,Input}from"ts-videos";const VIDEO_CODECS={h264:"h264",avc:"h264",avc1:"h264",h265:"h265",hevc:"h265",hvc1:"h265",hev1:"h265",vp8:"vp8",vp9:"vp9",av1:"av1",prores:"prores"};const AUDIO_CODECS={aac:"aac",mp3:"mp3",opus:"opus",vorbis:"vorbis",flac:"flac",alac:"alac",ac3:"ac3",eac3:"eac3"};export async function inspectVideo(bytes){const data=bytes instanceof Uint8Array?bytes:new Uint8Array(bytes);const input=new Input({source:new BufferSource(data),formats:[new Mp4InputFormat,new MovInputFormat,new WebmInputFormat]});try{const track=await input.getPrimaryVideoTrack();if(!track)return null;const audio=await input.getPrimaryAudioTrack().catch(()=>null);const format=await input.getFormatName().catch(()=>"");const duration=await input.getDuration().catch(()=>Number.NaN);const container=format==="webm"?"webm":"mp4";return{format,container,width:track.displayWidth||track.width,height:track.displayHeight||track.height,duration:Number.isFinite(duration)&&duration>0?Math.round(duration*1000)/1000:0,frameRate:track.frameRate&&track.frameRate>0?track.frameRate:30,videoCodec:VIDEO_CODECS[String(track.codec).toLowerCase()]??"unknown",...audio?{audioCodec:AUDIO_CODECS[String(audio.codec).toLowerCase()]??"unknown"}:{},hasAudio:!!audio}}catch{return null}finally{await input.close().catch(()=>{})}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/video",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.514",
5
+ "version": "0.75.518",
6
6
  "description": "Native video delivery planning for Stacks.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",