@stacksjs/video 0.75.515 → 0.75.519
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 +1 -1
- package/dist/index.js +4 -4
- package/dist/inspect.js +1 -1
- package/package.json +1 -1
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
|
|
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]
|
|
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!==
|
|
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
|
|
6
|
-
`),files,encrypted:!!protection}}export function videoAssetHeaders(path,bytes,etag,protectedMedia
|
|
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"}
|
|
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(()=>{})}}
|