@stacksjs/video 0.75.24 → 0.75.25
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.d.ts +25 -0
- package/dist/files.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/inspect.d.ts +14 -0
- package/dist/inspect.js +1 -0
- package/package.json +5 -3
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The range a `Range` header asks for within a file of `size` bytes: null for
|
|
3
|
+
* the whole file (no header, or one this does not read), 'unsatisfiable' for
|
|
4
|
+
* a range that starts past the end.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseByteRange(header: string | null | undefined, size: number): ByteRange | null | 'unsatisfiable';
|
|
7
|
+
/** The Content-Type for a video file by its name. */
|
|
8
|
+
export declare function videoContentType(name: string): string;
|
|
9
|
+
/** A Response for a stored video, 206 for a range, 416 past the end. */
|
|
10
|
+
export declare function videoFileResponse(file: Blob, name: string, options?: VideoFileResponseOptions): Response;
|
|
11
|
+
/**
|
|
12
|
+
* Serving a stored video file the way players ask for it. iOS requests
|
|
13
|
+
* `Range: bytes=0-1` first and will not play a file that answers with the
|
|
14
|
+
* whole body; every player seeks with ranges.
|
|
15
|
+
*/
|
|
16
|
+
export declare interface ByteRange {
|
|
17
|
+
start: number
|
|
18
|
+
end: number
|
|
19
|
+
}
|
|
20
|
+
export declare interface VideoFileResponseOptions {
|
|
21
|
+
range?: string | null
|
|
22
|
+
etag?: string
|
|
23
|
+
contentType?: string
|
|
24
|
+
protectedMedia?: boolean
|
|
25
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +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}`}})}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { ByteRange, VideoFileResponseOptions } from './files';
|
|
2
|
+
export type { VideoInspection } from './inspect';
|
|
1
3
|
export declare function deriveVideoLadder(profile: VideoProfile, maximum?: number): VideoRendition[];
|
|
2
4
|
export declare function video(source: string): VideoBuilder;
|
|
3
5
|
export declare function processVideoPlan(plan: VideoPlan, options?: VideoProcessOptions): Promise<ProcessedVideoDelivery>;
|
|
@@ -34,3 +36,5 @@ export declare class VideoBuilder {
|
|
|
34
36
|
generate(): VideoPlan;
|
|
35
37
|
process(options?: VideoProcessOptions): Promise<ProcessedVideoDelivery>;
|
|
36
38
|
}
|
|
39
|
+
export { parseByteRange, videoContentType, videoFileResponse } from './files';
|
|
40
|
+
export { inspectVideo } from './inspect';
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,4 @@ import{createHmac,timingSafeEqual}from"node:crypto";const edges=[240,360,480,540
|
|
|
3
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(`
|
|
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
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}
|
|
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";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { VideoProfile } from './index';
|
|
2
|
+
/** The profile of a video file, or null when it holds no video ts-videos can read. */
|
|
3
|
+
export declare function inspectVideo(bytes: Uint8Array | ArrayBuffer): Promise<VideoInspection | null>;
|
|
4
|
+
/**
|
|
5
|
+
* What a video file holds, read from its container with ts-videos: the
|
|
6
|
+
* profile `video(source).profile(...)` asks for, for an upload.
|
|
7
|
+
*
|
|
8
|
+
* ts-videos reads a container only through the formats it is handed, and its
|
|
9
|
+
* own list starts empty, so this hands it MP4, QuickTime and WebM. A phone's
|
|
10
|
+
* clip is QuickTime (`ftyp qt `), not MP4.
|
|
11
|
+
*/
|
|
12
|
+
export declare interface VideoInspection extends VideoProfile {
|
|
13
|
+
format: string
|
|
14
|
+
}
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +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(()=>{})}}
|
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.
|
|
5
|
+
"version": "0.75.25",
|
|
6
6
|
"description": "Native video delivery planning for Stacks.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
"prepublishOnly": "bun run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"ts-
|
|
35
|
-
"ts-videos": "^0.1.1"
|
|
34
|
+
"@ts-videos/mp4": "^0.1.1",
|
|
35
|
+
"@ts-videos/webm": "^0.1.1",
|
|
36
|
+
"ts-video-player": "^0.1.3",
|
|
37
|
+
"ts-videos": "^0.1.2"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"better-dx": "^0.2.24"
|