@bernierllc/content-type-video 1.0.0
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/.eslintrc.js +26 -0
- package/README.md +361 -0
- package/__tests__/VideoContentType.test.ts +522 -0
- package/dist/VideoContentType.d.ts +32 -0
- package/dist/VideoContentType.d.ts.map +1 -0
- package/dist/VideoContentType.js +154 -0
- package/dist/VideoContentType.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/LocalVideoStorage.d.ts +22 -0
- package/dist/storage/LocalVideoStorage.d.ts.map +1 -0
- package/dist/storage/LocalVideoStorage.js +155 -0
- package/dist/storage/LocalVideoStorage.js.map +1 -0
- package/dist/storage/VideoStorage.d.ts +12 -0
- package/dist/storage/VideoStorage.d.ts.map +1 -0
- package/dist/storage/VideoStorage.js +10 -0
- package/dist/storage/VideoStorage.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/jest.config.cjs +31 -0
- package/package.json +59 -0
- package/src/VideoContentType.ts +142 -0
- package/src/config.ts +63 -0
- package/src/index.ts +14 -0
- package/src/storage/LocalVideoStorage.ts +134 -0
- package/src/storage/VideoStorage.ts +20 -0
- package/src/types.ts +99 -0
- package/tsconfig.json +20 -0
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
preset: 'ts-jest',
|
|
11
|
+
testEnvironment: 'node',
|
|
12
|
+
roots: ['<rootDir>/__tests__'],
|
|
13
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
14
|
+
collectCoverageFrom: [
|
|
15
|
+
'src/**/*.ts',
|
|
16
|
+
'!src/**/*.d.ts',
|
|
17
|
+
'!src/index.ts'
|
|
18
|
+
],
|
|
19
|
+
coverageThreshold: {
|
|
20
|
+
global: {
|
|
21
|
+
branches: 75,
|
|
22
|
+
functions: 90,
|
|
23
|
+
lines: 90,
|
|
24
|
+
statements: 90
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
28
|
+
transform: {
|
|
29
|
+
'^.+\\.ts$': 'ts-jest'
|
|
30
|
+
}
|
|
31
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bernierllc/content-type-video",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Base video content type with file upload, local storage, and streaming URL generation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --watch",
|
|
10
|
+
"test:run": "jest",
|
|
11
|
+
"test:coverage": "jest --coverage",
|
|
12
|
+
"lint": "eslint src --ext .ts",
|
|
13
|
+
"clean": "rm -rf dist"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"content-type",
|
|
17
|
+
"video",
|
|
18
|
+
"media",
|
|
19
|
+
"upload",
|
|
20
|
+
"streaming",
|
|
21
|
+
"storage",
|
|
22
|
+
"metadata",
|
|
23
|
+
"bernierllc"
|
|
24
|
+
],
|
|
25
|
+
"author": "Bernier LLC",
|
|
26
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@bernierllc/content-type-registry": "^1.0.3",
|
|
29
|
+
"zod": "^3.22.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/jest": "^29.0.0",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
35
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
36
|
+
"eslint": "^8.0.0",
|
|
37
|
+
"jest": "^29.0.0",
|
|
38
|
+
"ts-jest": "^29.0.0",
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@bernierllc/neverhub-adapter": "^1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@bernierllc/neverhub-adapter": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"bernierllc": {
|
|
53
|
+
"integration": {
|
|
54
|
+
"neverhub": "optional",
|
|
55
|
+
"neveradmin": "not-applicable",
|
|
56
|
+
"logger": "planned"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { contentTypeRegistry, BaseContentType } from '@bernierllc/content-type-registry';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import {
|
|
13
|
+
VideoContentData,
|
|
14
|
+
VideoContentTypeConfig,
|
|
15
|
+
VideoContentResult,
|
|
16
|
+
VideoMetadataSchema,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { VideoStorage } from './storage/VideoStorage';
|
|
19
|
+
import { LocalVideoStorage } from './storage/LocalVideoStorage';
|
|
20
|
+
import { loadConfig } from './config';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Video content type with file upload, local storage, and streaming URL generation
|
|
24
|
+
*/
|
|
25
|
+
export class VideoContentType {
|
|
26
|
+
public readonly typeName = 'video';
|
|
27
|
+
public readonly version = '1.0.0';
|
|
28
|
+
private config: VideoContentTypeConfig;
|
|
29
|
+
private storage: VideoStorage;
|
|
30
|
+
|
|
31
|
+
constructor(config: Partial<VideoContentTypeConfig> = {}) {
|
|
32
|
+
this.config = loadConfig(config);
|
|
33
|
+
this.storage = new LocalVideoStorage(this.config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register video content type with registry
|
|
38
|
+
*/
|
|
39
|
+
async register(): Promise<void> {
|
|
40
|
+
contentTypeRegistry.register({
|
|
41
|
+
id: 'video',
|
|
42
|
+
name: 'Video',
|
|
43
|
+
description: 'Base video content type with file upload and streaming',
|
|
44
|
+
baseType: BaseContentType.VIDEO,
|
|
45
|
+
icon: 'video',
|
|
46
|
+
schema: VideoMetadataSchema,
|
|
47
|
+
editor: this.config.editor,
|
|
48
|
+
storage: this.config.storage,
|
|
49
|
+
publishing: this.config.publishing,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate video file
|
|
55
|
+
*/
|
|
56
|
+
async validate(file: Buffer | string, filename: string): Promise<VideoContentResult<void>> {
|
|
57
|
+
// Check for zero-byte files
|
|
58
|
+
const fileSize = Buffer.isBuffer(file) ? file.length : fs.statSync(file).size;
|
|
59
|
+
if (fileSize === 0) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: 'File is empty (0 bytes)',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate file format
|
|
67
|
+
const extension = path.extname(filename).slice(1).toLowerCase();
|
|
68
|
+
const allowedFormats = this.config.uploadOptions?.allowedFormats || ['mp4', 'webm', 'mov', 'avi'];
|
|
69
|
+
|
|
70
|
+
if (!allowedFormats.includes(extension)) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: `File format ${extension} not allowed. Allowed: ${allowedFormats.join(', ')}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate file size
|
|
78
|
+
const maxSize = this.config.uploadOptions?.maxSizeBytes || 500 * 1024 * 1024;
|
|
79
|
+
if (fileSize > maxSize) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: `File size ${fileSize} exceeds maximum ${maxSize}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { success: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Upload video file and extract metadata
|
|
91
|
+
*/
|
|
92
|
+
async upload(
|
|
93
|
+
file: Buffer | string,
|
|
94
|
+
filename: string,
|
|
95
|
+
uploadedBy?: string
|
|
96
|
+
): Promise<VideoContentResult<VideoContentData>> {
|
|
97
|
+
try {
|
|
98
|
+
const validation = await this.validate(file, filename);
|
|
99
|
+
if (!validation.success) {
|
|
100
|
+
return { success: false, error: validation.error || 'Validation failed' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return await this.storage.save(file, filename, uploadedBy);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
error: error instanceof Error ? error.message : 'Unknown error during video upload',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async get(localPath: string): Promise<Buffer | null> {
|
|
113
|
+
return this.storage.get(localPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async delete(localPath: string): Promise<boolean> {
|
|
117
|
+
return this.storage.delete(localPath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async exists(localPath: string): Promise<boolean> {
|
|
121
|
+
return this.storage.exists(localPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getStreamingUrl(localPath: string): string {
|
|
125
|
+
return this.storage.getStreamingUrl(localPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validate video content data
|
|
130
|
+
*/
|
|
131
|
+
validateContent(data: VideoContentData): VideoContentResult<VideoContentData> {
|
|
132
|
+
try {
|
|
133
|
+
VideoMetadataSchema.parse(data.metadata);
|
|
134
|
+
return { success: true, data };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: error instanceof Error ? error.message : 'Validation failed',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VideoContentTypeConfig } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load configuration from environment variables with fallback to defaults
|
|
13
|
+
*/
|
|
14
|
+
export function loadConfig(config: Partial<VideoContentTypeConfig> = {}): VideoContentTypeConfig {
|
|
15
|
+
const maxSizeBytes = config.uploadOptions?.maxSizeBytes
|
|
16
|
+
|| (process.env.VIDEO_MAX_SIZE_BYTES ? parseInt(process.env.VIDEO_MAX_SIZE_BYTES, 10) : undefined)
|
|
17
|
+
|| 500 * 1024 * 1024; // 500MB default
|
|
18
|
+
|
|
19
|
+
const storageBasePath = config.uploadOptions?.storageBasePath
|
|
20
|
+
|| process.env.VIDEO_STORAGE_BASE_PATH
|
|
21
|
+
|| './storage/videos';
|
|
22
|
+
|
|
23
|
+
const streamingBaseUrl = config.streamingBaseUrl
|
|
24
|
+
|| process.env.VIDEO_STREAMING_BASE_URL
|
|
25
|
+
|| '/api/video/stream';
|
|
26
|
+
|
|
27
|
+
const allowedFormats = config.uploadOptions?.allowedFormats
|
|
28
|
+
|| (process.env.VIDEO_ALLOWED_FORMATS ? process.env.VIDEO_ALLOWED_FORMATS.split(',').map(f => f.trim()) : undefined)
|
|
29
|
+
|| ['mp4', 'webm', 'mov', 'avi'];
|
|
30
|
+
|
|
31
|
+
const generateThumbnail = config.uploadOptions?.generateThumbnail
|
|
32
|
+
!== undefined ? config.uploadOptions.generateThumbnail
|
|
33
|
+
: (process.env.VIDEO_GENERATE_THUMBNAIL === 'true' || true);
|
|
34
|
+
|
|
35
|
+
const extractMetadata = config.uploadOptions?.extractMetadata
|
|
36
|
+
!== undefined ? config.uploadOptions.extractMetadata
|
|
37
|
+
: (process.env.VIDEO_EXTRACT_METADATA === 'true' || true);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
baseType: 'video',
|
|
41
|
+
editor: config.editor || {
|
|
42
|
+
component: 'file-upload-with-player',
|
|
43
|
+
settings: {},
|
|
44
|
+
},
|
|
45
|
+
storage: config.storage || {
|
|
46
|
+
method: 'file',
|
|
47
|
+
settings: {},
|
|
48
|
+
},
|
|
49
|
+
publishing: config.publishing || {
|
|
50
|
+
methods: ['streaming-url'],
|
|
51
|
+
defaultMethod: 'streaming-url',
|
|
52
|
+
settings: {},
|
|
53
|
+
},
|
|
54
|
+
uploadOptions: {
|
|
55
|
+
maxSizeBytes,
|
|
56
|
+
allowedFormats,
|
|
57
|
+
generateThumbnail,
|
|
58
|
+
extractMetadata,
|
|
59
|
+
storageBasePath,
|
|
60
|
+
},
|
|
61
|
+
streamingBaseUrl,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './types';
|
|
10
|
+
export * from './VideoContentType';
|
|
11
|
+
export * from './config';
|
|
12
|
+
export * from './storage/VideoStorage';
|
|
13
|
+
export * from './storage/LocalVideoStorage';
|
|
14
|
+
export { VideoContentType } from './VideoContentType';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs/promises';
|
|
10
|
+
import * as fsSync from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { VideoStorage } from './VideoStorage';
|
|
13
|
+
import { VideoContentResult, VideoContentData, VideoMetadata, VideoMetadataSchema, VideoContentTypeConfig } from '../types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Local file system storage implementation for videos
|
|
17
|
+
*/
|
|
18
|
+
export class LocalVideoStorage implements VideoStorage {
|
|
19
|
+
private storageBasePath: string;
|
|
20
|
+
private streamingBaseUrl: string;
|
|
21
|
+
|
|
22
|
+
constructor(config: VideoContentTypeConfig) {
|
|
23
|
+
this.storageBasePath = config.uploadOptions?.storageBasePath || './storage/videos';
|
|
24
|
+
this.streamingBaseUrl = config.streamingBaseUrl || '/api/video/stream';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async save(file: Buffer | string, filename: string, uploadedBy?: string): Promise<VideoContentResult<VideoContentData>> {
|
|
28
|
+
try {
|
|
29
|
+
// Ensure storage directory exists
|
|
30
|
+
await fs.mkdir(this.storageBasePath, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Generate unique filename
|
|
33
|
+
const timestamp = Date.now();
|
|
34
|
+
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
35
|
+
const uniqueFilename = `${timestamp}-${sanitizedFilename}`;
|
|
36
|
+
|
|
37
|
+
// Save file
|
|
38
|
+
const filePath = path.join(this.storageBasePath, uniqueFilename);
|
|
39
|
+
|
|
40
|
+
if (Buffer.isBuffer(file)) {
|
|
41
|
+
await fs.writeFile(filePath, file);
|
|
42
|
+
} else {
|
|
43
|
+
await fs.copyFile(file, filePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract metadata
|
|
47
|
+
const metadata = await this.extractMetadata(filePath);
|
|
48
|
+
|
|
49
|
+
// Generate streaming URL
|
|
50
|
+
const streamingUrl = this.getStreamingUrl(filePath);
|
|
51
|
+
|
|
52
|
+
const contentData: VideoContentData = {
|
|
53
|
+
filePath,
|
|
54
|
+
streamingUrl,
|
|
55
|
+
metadata,
|
|
56
|
+
uploadedAt: new Date(),
|
|
57
|
+
uploadedBy,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
data: contentData,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: error instanceof Error ? error.message : 'Unknown save error',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async get(localPath: string): Promise<Buffer | null> {
|
|
73
|
+
try {
|
|
74
|
+
return await fs.readFile(localPath);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(localPath: string): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
await fs.unlink(localPath);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async exists(localPath: string): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(localPath);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getStreamingUrl(localPath: string): string {
|
|
99
|
+
const relativePath = path.relative(this.storageBasePath, localPath);
|
|
100
|
+
return `${this.streamingBaseUrl}/${encodeURIComponent(relativePath)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract video metadata
|
|
105
|
+
* Note: In production, this would use ffmpeg/ffprobe for real metadata extraction
|
|
106
|
+
*/
|
|
107
|
+
private async extractMetadata(filePath: string): Promise<VideoMetadata> {
|
|
108
|
+
const stats = fsSync.statSync(filePath);
|
|
109
|
+
|
|
110
|
+
// Basic metadata - in production, use ffmpeg/ffprobe for real extraction
|
|
111
|
+
const metadata: VideoMetadata = {
|
|
112
|
+
filename: path.basename(filePath),
|
|
113
|
+
mimeType: this.getMimeTypeFromExtension(filePath),
|
|
114
|
+
size: stats.size,
|
|
115
|
+
duration: 60, // Placeholder - would extract from video
|
|
116
|
+
width: 1920, // Placeholder - would extract from video
|
|
117
|
+
height: 1080, // Placeholder - would extract from video
|
|
118
|
+
fps: 30, // Placeholder - would extract from video
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return VideoMetadataSchema.parse(metadata);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private getMimeTypeFromExtension(filePath: string): string {
|
|
125
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
126
|
+
const mimeTypes: Record<string, string> = {
|
|
127
|
+
mp4: 'video/mp4',
|
|
128
|
+
webm: 'video/webm',
|
|
129
|
+
mov: 'video/quicktime',
|
|
130
|
+
avi: 'video/x-msvideo',
|
|
131
|
+
};
|
|
132
|
+
return mimeTypes[ext] || 'video/mp4';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VideoContentResult, VideoContentData } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Video storage interface
|
|
13
|
+
*/
|
|
14
|
+
export interface VideoStorage {
|
|
15
|
+
save(file: Buffer | string, filename: string, uploadedBy?: string): Promise<VideoContentResult<VideoContentData>>;
|
|
16
|
+
get(localPath: string): Promise<Buffer | null>;
|
|
17
|
+
delete(localPath: string): Promise<boolean>;
|
|
18
|
+
exists(localPath: string): Promise<boolean>;
|
|
19
|
+
getStreamingUrl(localPath: string): string;
|
|
20
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Video metadata schema
|
|
13
|
+
*/
|
|
14
|
+
export const VideoMetadataSchema = z.object({
|
|
15
|
+
filename: z.string(),
|
|
16
|
+
mimeType: z.string().regex(/^video\//),
|
|
17
|
+
size: z.number().positive(),
|
|
18
|
+
duration: z.number().positive(), // seconds
|
|
19
|
+
width: z.number().int().positive(),
|
|
20
|
+
height: z.number().int().positive(),
|
|
21
|
+
fps: z.number().positive(),
|
|
22
|
+
thumbnail: z.string().optional(), // base64 or URL to thumbnail
|
|
23
|
+
bitrate: z.number().positive().optional(),
|
|
24
|
+
codec: z.string().optional()
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type VideoMetadata = z.infer<typeof VideoMetadataSchema>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Video content data
|
|
31
|
+
*/
|
|
32
|
+
export interface VideoContentData {
|
|
33
|
+
filePath: string; // Local file path
|
|
34
|
+
streamingUrl: string; // Generated streaming URL
|
|
35
|
+
metadata: VideoMetadata;
|
|
36
|
+
uploadedAt: Date;
|
|
37
|
+
uploadedBy?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Video upload options
|
|
42
|
+
*/
|
|
43
|
+
export interface VideoUploadOptions {
|
|
44
|
+
maxSizeBytes?: number; // Default: 500MB
|
|
45
|
+
allowedFormats?: string[]; // Default: ['mp4', 'webm', 'mov', 'avi']
|
|
46
|
+
generateThumbnail?: boolean; // Default: true
|
|
47
|
+
extractMetadata?: boolean; // Default: true
|
|
48
|
+
storageBasePath?: string; // Default: './storage/videos'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Video content type configuration
|
|
53
|
+
*/
|
|
54
|
+
export interface VideoContentTypeConfig {
|
|
55
|
+
baseType: 'video';
|
|
56
|
+
editor: {
|
|
57
|
+
component: string;
|
|
58
|
+
settings?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
storage: {
|
|
61
|
+
method: string;
|
|
62
|
+
settings?: Record<string, unknown>;
|
|
63
|
+
};
|
|
64
|
+
publishing: {
|
|
65
|
+
methods: string[];
|
|
66
|
+
defaultMethod?: string;
|
|
67
|
+
settings?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
uploadOptions?: VideoUploadOptions;
|
|
70
|
+
streamingBaseUrl?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Result type for video operations
|
|
75
|
+
*/
|
|
76
|
+
export interface VideoContentResult<T = unknown> {
|
|
77
|
+
success: boolean;
|
|
78
|
+
data?: T;
|
|
79
|
+
error?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Video content error codes
|
|
84
|
+
*/
|
|
85
|
+
export type VideoContentErrorCode =
|
|
86
|
+
| 'INVALID_FORMAT'
|
|
87
|
+
| 'FILE_TOO_LARGE'
|
|
88
|
+
| 'UPLOAD_FAILED'
|
|
89
|
+
| 'METADATA_EXTRACTION_FAILED'
|
|
90
|
+
| 'VALIDATION_FAILED';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Video content error
|
|
94
|
+
*/
|
|
95
|
+
export interface VideoContentError {
|
|
96
|
+
code: VideoContentErrorCode;
|
|
97
|
+
message: string;
|
|
98
|
+
details?: unknown;
|
|
99
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"noEmit": false
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
"**/*.test.ts",
|
|
18
|
+
"**/*.test.tsx"
|
|
19
|
+
]
|
|
20
|
+
}
|