Skip to content

Commit 27dcdac

Browse files
committed
feat(video): execute native delivery plans
1 parent 05da8d8 commit 27dcdac

3 files changed

Lines changed: 41 additions & 3 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"name":"@stacksjs/video","type":"module","sideEffects":false,"version":"0.70.162","description":"Native video delivery planning for Stacks.","author":"Chris Breuer","license":"MIT","funding":"https://github.com/sponsors/chrisbbreuer","repository":{"type":"git","url":"git+https://github.com/stacksjs/stacks.git","directory":"./storage/framework/core/video"},"exports":{".":{"types":"./dist/index.d.ts","development":"./src/index.ts","bun":"./dist/index.js","import":"./dist/index.js"}},"module":"dist/index.js","types":"dist/index.d.ts","files":["README.md","dist"],"scripts":{"build":"bun build.ts","typecheck":"bun tsc --noEmit","prepublishOnly":"bun run build"},"dependencies":{"ts-video-player":"^0.1.0"},"devDependencies":{"better-dx":"catalog:"}}
1+
{"name":"@stacksjs/video","type":"module","sideEffects":false,"version":"0.70.162","description":"Native video delivery planning for Stacks.","author":"Chris Breuer","license":"MIT","funding":"https://github.com/sponsors/chrisbbreuer","repository":{"type":"git","url":"git+https://github.com/stacksjs/stacks.git","directory":"./storage/framework/core/video"},"exports":{".":{"types":"./dist/index.d.ts","development":"./src/index.ts","bun":"./dist/index.js","import":"./dist/index.js"}},"module":"dist/index.js","types":"dist/index.d.ts","files":["README.md","dist"],"scripts":{"build":"bun build.ts","typecheck":"bun tsc --noEmit","prepublishOnly":"bun run build"},"dependencies":{"ts-video-player":"^0.1.0","ts-videos":"^0.0.0"},"devDependencies":{"better-dx":"catalog:"}}

storage/framework/core/video/src/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import { createHmac, timingSafeEqual } from 'node:crypto'
22

33
export type VideoContainer = 'mp4' | 'webm'
44
export type StreamingFormat = 'hls' | 'dash'
5-
export interface VideoProfile { width: number, height: number, duration: number, frameRate: number, container: VideoContainer, videoCodec: string, audioCodec?: string, videoBitrate?: number, hasAudio?: boolean, hdr?: boolean }
5+
export type VideoCodec = 'h264' | 'h265' | 'vp8' | 'vp9' | 'av1' | 'mpeg1' | 'mpeg2' | 'mpeg4' | 'theora' | 'mjpeg' | 'prores' | 'dnxhd' | 'unknown'
6+
export type VideoAudioCodec = 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac' | 'alac' | 'ac3' | 'eac3' | 'dts' | 'pcm_s16le' | 'pcm_s16be' | 'pcm_s24le' | 'pcm_s24be' | 'pcm_s32le' | 'pcm_s32be' | 'pcm_f32le' | 'pcm_f32be' | 'pcm_f64le' | 'pcm_f64be' | 'pcm_mulaw' | 'pcm_alaw' | 'unknown'
7+
export interface VideoProfile { width: number, height: number, duration: number, frameRate: number, container: VideoContainer, videoCodec: VideoCodec, audioCodec?: VideoAudioCodec, videoBitrate?: number, hasAudio?: boolean, hdr?: boolean }
68
export interface VideoRendition { name: string, width: number, height: number, frameRate: number, videoBitrate: number, audioBitrate: number }
79
export interface VideoCapabilities { videoEncoder: boolean, audioEncoder: boolean, videoCodecs: string[], audioCodecs: string[] }
8-
export interface VideoOutput { container: VideoContainer, videoCodec: string, audioCodec?: string, action: 'copy' | 'transcode', available: boolean, reason?: string }
10+
export interface VideoOutput { container: VideoContainer, videoCodec: VideoCodec, audioCodec?: VideoAudioCodec, action: 'copy' | 'transcode', available: boolean, reason?: string }
911
export interface VideoPlan { source: string, profile: VideoProfile, renditions: VideoRendition[], outputs: VideoOutput[], streaming: StreamingFormat[], segmentDuration: number, keyframeInterval: number }
12+
export interface VideoProcessOptions { batchSize?: number, signal?: AbortSignal }
13+
export interface ProcessedVideoDerivative { output: VideoOutput, rendition: VideoRendition, bytes: Uint8Array }
1014
export interface PreviewCue { startTime: number, endTime: number, uri: string, x?: number, y?: number, width?: number, height?: number }
1115
const edges = [240, 360, 480, 540, 720, 1080, 1440, 2160]
1216
const even = (value: number): number => Math.max(2, Math.round(value / 2) * 2)
@@ -54,8 +58,21 @@ export class VideoBuilder {
5458
const segmentDuration = profile.duration <= 30 ? 2 : profile.duration <= 600 ? 4 : 6
5559
return { source: this.source, profile, renditions, outputs, streaming: this.streams, segmentDuration, keyframeInterval: Math.max(1, Math.round(profile.frameRate * segmentDuration)) }
5660
}
61+
async process(options: VideoProcessOptions = {}): Promise<ProcessedVideoDerivative[]> { return processVideoPlan(this.generate(), options) }
5762
}
5863
export function video(source: string): VideoBuilder { return new VideoBuilder(source) }
64+
export async function processVideoPlan(plan: VideoPlan, options: VideoProcessOptions = {}): Promise<ProcessedVideoDerivative[]> {
65+
assertVideoPlanExecutable(plan)
66+
const { generateVideoDerivatives } = await import('ts-videos/native-transcode')
67+
return generateVideoDerivatives(plan.source, {
68+
source: plan.profile,
69+
renditions: plan.renditions,
70+
outputs: plan.outputs,
71+
streaming: plan.streaming,
72+
segmentDuration: plan.segmentDuration,
73+
keyframeInterval: plan.keyframeInterval,
74+
}, options)
75+
}
5976
export function assertVideoPlanExecutable(plan: VideoPlan): void {
6077
const missing = plan.outputs.filter(value => !value.available)
6178
if (missing.length) throw new Error(missing.map(value => `${value.container}: ${value.reason}`).join('; '))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect, mock, test } from 'bun:test'
2+
3+
mock.module('ts-videos/native-transcode', () => ({
4+
generateVideoDerivatives: async (source: string, plan: { source: { width: number }, renditions: unknown[] }): Promise<unknown[]> => {
5+
expect(source).toBe('movie.mp4')
6+
expect(plan.source.width).toBe(1920)
7+
expect(plan.renditions.length).toBeGreaterThan(1)
8+
return [{ output: { container: 'mp4' }, rendition: plan.renditions[0], bytes: new Uint8Array([1, 2, 3]) }]
9+
},
10+
}))
11+
12+
const { video } = await import('../src')
13+
14+
test('processes a generated video plan through ts-videos', async () => {
15+
const result = await video('movie.mp4')
16+
.profile({ width: 1920, height: 1080, duration: 60, frameRate: 30, container: 'mp4', videoCodec: 'h264', audioCodec: 'aac' })
17+
.output(['mp4'])
18+
.runtime({ videoEncoder: true, audioEncoder: true, videoCodecs: ['h264'], audioCodecs: ['aac'] })
19+
.process()
20+
expect(result[0]?.bytes).toEqual(new Uint8Array([1, 2, 3]))
21+
})

0 commit comments

Comments
 (0)